-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathDharmaSmartWalletImplementationV7.sol
2225 lines (2030 loc) · 94.8 KB
/
DharmaSmartWalletImplementationV7.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
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
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
pragma solidity 0.5.11; // optimization runs: 200, evm version: petersburg
// WARNING - `executeActionWithAtomicBatchCalls` has a `bytes[]` argument that
// requires ABIEncoderV2. Exercise caution when calling that specific function.
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../../interfaces/DharmaSmartWalletImplementationV1Interface.sol";
import "../../../interfaces/DharmaSmartWalletImplementationV3Interface.sol";
import "../../../interfaces/DharmaSmartWalletImplementationV4Interface.sol";
import "../../../interfaces/DharmaSmartWalletImplementationV7Interface.sol";
import "../../../interfaces/CTokenInterface.sol";
import "../../../interfaces/DTokenInterface.sol";
import "../../../interfaces/USDCV1Interface.sol";
import "../../../interfaces/DharmaKeyRegistryInterface.sol";
import "../../../interfaces/DharmaEscapeHatchRegistryInterface.sol";
import "../../../interfaces/ERC1271.sol";
import "../../../interfaces/SaiToDaiMigratorInterface.sol";
import "../../helpers/SmartWalletRevertReasonHelperV1.sol";
/**
* @title DharmaSmartWalletImplementationV7
* @author 0age
* @notice The V7 implementation for the Dharma smart wallet is a non-custodial,
* meta-transaction-enabled wallet with helper functions to facilitate lending
* funds through Dharma Dai and Dharma USD Coin (which in turn use CompoundV2),
* and with an added security backstop provided by Dharma Labs prior to making
* withdrawals. It adds support for Dharma Dai and Dharma USD Coin - they employ
* the respective cTokens as backing tokens and mint and redeem them internally
* as interest-bearing collateral. This implementation also contains methods to
* support account recovery, escape hatch functionality, and generic actions,
* including in an atomic batch. The smart wallet instances utilizing this
* implementation are deployed through the Dharma Smart Wallet Factory via
* `CREATE2`, which allows for their address to be known ahead of time, and any
* Dai or USDC that has already been sent into that address will automatically
* be deposited into the respective Dharma Token upon deployment of the new
* smart wallet instance.
*/
contract DharmaSmartWalletImplementationV7 is
DharmaSmartWalletImplementationV1Interface,
DharmaSmartWalletImplementationV3Interface,
DharmaSmartWalletImplementationV4Interface,
DharmaSmartWalletImplementationV7Interface,
ERC1271 {
using Address for address;
using ECDSA for bytes32;
// WARNING: DO NOT REMOVE OR REORDER STORAGE WHEN WRITING NEW IMPLEMENTATIONS!
// The user signing key associated with this account is in storage slot 0.
// It is the core differentiator when it comes to the account in question.
address private _userSigningKey;
// The nonce associated with this account is in storage slot 1. Every time a
// signature is submitted, it must have the appropriate nonce, and once it has
// been accepted the nonce will be incremented.
uint256 private _nonce;
// The self-call context flag is in storage slot 2. Some protected functions
// may only be called externally from calls originating from other methods on
// this contract, which enables appropriate exception handling on reverts.
// Any storage should only be set immediately preceding a self-call and should
// be cleared upon entering the protected function being called.
bytes4 internal _selfCallContext;
// END STORAGE DECLARATIONS - DO NOT REMOVE OR REORDER STORAGE ABOVE HERE!
// The smart wallet version will be used when constructing valid signatures.
uint256 internal constant _DHARMA_SMART_WALLET_VERSION = 7;
// DharmaKeyRegistryV2 holds a public key for verifying meta-transactions.
DharmaKeyRegistryInterface internal constant _DHARMA_KEY_REGISTRY = (
DharmaKeyRegistryInterface(0x000000000D38df53b45C5733c7b34000dE0BDF52)
);
// Account recovery is facilitated using a hard-coded recovery manager,
// controlled by Dharma and implementing appropriate timelocks.
address internal constant _ACCOUNT_RECOVERY_MANAGER = address(
0x0000000000DfEd903aD76996FC07BF89C0127B1E
);
// Users can designate an "escape hatch" account with the ability to sweep all
// funds from their smart wallet by using the Dharma Escape Hatch Registry.
DharmaEscapeHatchRegistryInterface internal constant _ESCAPE_HATCH_REGISTRY = (
DharmaEscapeHatchRegistryInterface(0x00000000005280B515004B998a944630B6C663f8)
);
// Interface with dDai, dUSDC, Dai, USDC, Sai, cSai, cDai, cUSDC, & migrator.
DTokenInterface internal constant _DDAI = DTokenInterface(
0x00000000001876eB1444c986fD502e618c587430 // mainnet
);
DTokenInterface internal constant _DUSDC = DTokenInterface(
0x00000000008943c65cAf789FFFCF953bE156f6f8 // mainnet
);
IERC20 internal constant _DAI = IERC20(
0x6B175474E89094C44Da98b954EedeAC495271d0F // mainnet
);
IERC20 internal constant _USDC = IERC20(
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 // mainnet
);
IERC20 internal constant _SAI = IERC20(
0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359 // mainnet
);
CTokenInterface internal constant _CSAI = CTokenInterface(
0xF5DCe57282A584D2746FaF1593d3121Fcac444dC // mainnet
);
CTokenInterface internal constant _CDAI = CTokenInterface(
0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 // mainnet
);
CTokenInterface internal constant _CUSDC = CTokenInterface(
0x39AA39c021dfbaE8faC545936693aC917d5E7563 // mainnet
);
SaiToDaiMigratorInterface internal constant _MIGRATOR = SaiToDaiMigratorInterface(
0xc73e0383F3Aff3215E6f04B0331D58CeCf0Ab849 // mainnet
);
USDCV1Interface internal constant _USDC_NAUGHTY = USDCV1Interface(
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 // mainnet
);
// The "revert reason helper" contains a collection of revert reason strings.
SmartWalletRevertReasonHelperV1 internal constant _REVERT_REASON_HELPER = (
SmartWalletRevertReasonHelperV1(0xE24257338d0c15f3Dd00Ed59fcA9e50CfB167bA8)
);
// Compound returns a value of 0 to indicate success, or lack of an error.
uint256 internal constant _COMPOUND_SUCCESS = 0;
// ERC-1271 must return this magic value when `isValidSignature` is called.
bytes4 internal constant _ERC_1271_MAGIC_VALUE = bytes4(0x20c13b0b);
// Minimum supported deposit & non-maximum withdrawal size is .001 underlying.
uint256 private constant _JUST_UNDER_ONE_1000th_DAI = 999999999999999;
uint256 private constant _JUST_UNDER_ONE_1000th_USDC = 999;
// Specify the amount of gas to supply when making Ether transfers.
uint256 private constant _ETH_TRANSFER_GAS = 4999;
/**
* @notice In the initializer, set up the initial user signing key, set
* approval on the Dharma Dai and Dharma USD Coin contracts, and deposit any
* Dai or USDC already at this address to receive dDai or dUSDC. Note that
* this initializer is only callable while the smart wallet instance is still
* in the contract creation phase.
* @param userSigningKey address The initial user signing key for the smart
* wallet.
*/
function initialize(address userSigningKey) external {
// Ensure that this function is only callable during contract construction.
assembly { if extcodesize(address) { revert(0, 0) } }
// Set up the user's signing key and emit a corresponding event.
_setUserSigningKey(userSigningKey);
// Approve the dDai contract to transfer Dai on behalf of this contract.
if (_setFullApproval(AssetType.DAI)) {
// Get the current Dai balance on this contract.
uint256 daiBalance = _DAI.balanceOf(address(this));
// Try to deposit the full Dai balance to Dharma Dai.
_depositDharmaToken(AssetType.DAI, daiBalance);
}
// Approve the dUSDC contract to transfer USDC on behalf of this contract.
if (_setFullApproval(AssetType.USDC)) {
// Get the current USDC balance on this contract.
uint256 usdcBalance = _USDC.balanceOf(address(this));
// Try to deposit the full Dai balance to Dharma USDC.
_depositDharmaToken(AssetType.USDC, usdcBalance);
}
}
/**
* @notice Deposit all Dai and USDC currently residing at this address and
* receive Dharma Dai or Dharma USD Coin in return. Note that "repay" is not
* currently implemented, though it may be in a future implementation. If some
* step of this function fails, the function itself will still succeed, but an
* `ExternalError` with information on what went wrong will be emitted.
*/
function repayAndDeposit() external {
// Get the current Dai balance on this contract.
uint256 daiBalance = _DAI.balanceOf(address(this));
// If there is any Dai balance, check for adequate approval for dDai.
if (daiBalance > 0) {
uint256 daiAllowance = _DAI.allowance(address(this), address(_DDAI));
// If allowance is insufficient, try to set it before depositing.
if (daiAllowance < daiBalance) {
if (_setFullApproval(AssetType.DAI)) {
// Deposit the full available Dai balance to Dharma Dai.
_depositDharmaToken(AssetType.DAI, daiBalance);
}
// Otherwise, just go ahead and try the Dai deposit.
} else {
// Deposit the full available Dai balance to Dharma Dai.
_depositDharmaToken(AssetType.DAI, daiBalance);
}
}
// Get the current USDC balance on this contract.
uint256 usdcBalance = _USDC.balanceOf(address(this));
// If there is any USDC balance, check for adequate approval for dUSDC.
if (usdcBalance > 0) {
uint256 usdcAllowance = _USDC.allowance(address(this), address(_DUSDC));
// If allowance is insufficient, try to set it before depositing.
if (usdcAllowance < usdcBalance) {
if (_setFullApproval(AssetType.USDC)) {
// Deposit the full available USDC balance to Dharma USDC.
_depositDharmaToken(AssetType.USDC, usdcBalance);
}
// Otherwise, just go ahead and try the USDC deposit.
} else {
// Deposit the full available USDC balance to Dharma USDC.
_depositDharmaToken(AssetType.USDC, usdcBalance);
}
}
}
/**
* @notice Withdraw Dai to a provided recipient address by redeeming the
* underlying Dai from the dDai contract and transferring it to the recipient.
* All Dai in Dharma Dai and in the smart wallet itself can be withdrawn by
* providing an amount of uint256(-1) or 0xfff...fff. This function can be
* called directly by the account set as the global key on the Dharma Key
* Registry, or by any relayer that provides a signed message from the same
* keyholder. The nonce used for the signature must match the current nonce on
* the smart wallet, and gas supplied to the call must exceed the specified
* minimum action gas, plus the gas that will be spent before the gas check is
* reached - usually somewhere around 25,000 gas. If the withdrawal fails, an
* `ExternalError` with additional details on what went wrong will be emitted.
* Note that some dust may still be left over, even in the event of a max
* withdrawal, due to the fact that Dai has a higher precision than dDai. Also
* note that the withdrawal will fail in the event that Compound does not have
* sufficient Dai available to withdraw.
* @param amount uint256 The amount of Dai to withdraw.
* @param recipient address The account to transfer the withdrawn Dai to.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
* @return True if the withdrawal succeeded, otherwise false.
*/
function withdrawDai(
uint256 amount,
address recipient,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external returns (bool ok) {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.DAIWithdrawal,
abi.encode(amount, recipient),
minimumActionGas,
userSignature,
dharmaSignature
);
// Ensure that an amount of at least 0.001 Dai has been supplied.
require(amount > _JUST_UNDER_ONE_1000th_DAI, _revertReason(0));
// Ensure that a non-zero recipient has been supplied.
require(recipient != address(0), _revertReason(1));
// Set the self-call context in order to call _withdrawDaiAtomic.
_selfCallContext = this.withdrawDai.selector;
// Make the atomic self-call - if redeemUnderlying fails on dDai, it will
// succeed but nothing will happen except firing an ExternalError event. If
// the second part of the self-call (the Dai transfer) fails, it will revert
// and roll back the first part of the call as well as fire an ExternalError
// event after returning from the failed call.
bytes memory returnData;
(ok, returnData) = address(this).call(abi.encodeWithSelector(
this._withdrawDaiAtomic.selector, amount, recipient
));
// If the atomic call failed, emit an event signifying a transfer failure.
if (!ok) {
emit ExternalError(address(_DAI), _revertReason(2));
} else {
// Set ok to false if the call succeeded but the withdrawal failed.
ok = abi.decode(returnData, (bool));
}
}
/**
* @notice Protected function that can only be called from `withdrawDai` on
* this contract. It will attempt to withdraw the supplied amount of Dai, or
* the maximum amount if specified using `uint256(-1)`, to the supplied
* recipient address by redeeming the underlying Dai from the dDai contract
* and transferring it to the recipient. An ExternalError will be emitted and
* the transfer will be skipped if the call to `redeem` or `redeemUnderlying`
* fails, and any revert will be caught by `withdrawDai` and diagnosed in
* order to emit an appropriate `ExternalError` as well.
* @param amount uint256 The amount of Dai to withdraw.
* @param recipient address The account to transfer the withdrawn Dai to.
* @return True if the withdrawal succeeded, otherwise false.
*/
function _withdrawDaiAtomic(
uint256 amount,
address recipient
) external returns (bool success) {
// Ensure caller is this contract and self-call context is correctly set.
_enforceSelfCallFrom(this.withdrawDai.selector);
// If amount = 0xfff...fff, withdraw the maximum amount possible.
bool maxWithdraw = (amount == uint256(-1));
if (maxWithdraw) {
// First attempt to redeem all dDai if there is a balance.
_withdrawMaxFromDharmaToken(AssetType.DAI);
// Then transfer all Dai to recipient if there is a balance.
require(_transferMax(_DAI, recipient, false));
success = true;
} else {
// Attempt to withdraw specified Dai from Dharma Dai before proceeding.
if (_withdrawFromDharmaToken(AssetType.DAI, amount)) {
// At this point Dai transfer should never fail - wrap it just in case.
require(_DAI.transfer(recipient, amount));
success = true;
}
}
}
/**
* @notice Withdraw USDC to a provided recipient address by redeeming the
* underlying USDC from the dUSDC contract and transferring it to recipient.
* All USDC in Dharma USD Coin and in the smart wallet itself can be withdrawn
* by providing an amount of uint256(-1) or 0xfff...fff. This function can be
* called directly by the account set as the global key on the Dharma Key
* Registry, or by any relayer that provides a signed message from the same
* keyholder. The nonce used for the signature must match the current nonce on
* the smart wallet, and gas supplied to the call must exceed the specified
* minimum action gas, plus the gas that will be spent before the gas check is
* reached - usually somewhere around 25,000 gas. If the withdrawal fails, an
* `ExternalError` with additional details on what went wrong will be emitted.
* Note that the USDC contract can be paused and also allows for blacklisting
* accounts - either of these possibilities may cause a withdrawal to fail. In
* addition, Compound may not have sufficient USDC available at the time to
* withdraw.
* @param amount uint256 The amount of USDC to withdraw.
* @param recipient address The account to transfer the withdrawn USDC to.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
* @return True if the withdrawal succeeded, otherwise false.
*/
function withdrawUSDC(
uint256 amount,
address recipient,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external returns (bool ok) {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.USDCWithdrawal,
abi.encode(amount, recipient),
minimumActionGas,
userSignature,
dharmaSignature
);
// Ensure that an amount of at least 0.001 USDC has been supplied.
require(amount > _JUST_UNDER_ONE_1000th_USDC, _revertReason(3));
// Ensure that a non-zero recipient has been supplied.
require(recipient != address(0), _revertReason(1));
// Set the self-call context in order to call _withdrawUSDCAtomic.
_selfCallContext = this.withdrawUSDC.selector;
// Make the atomic self-call - if redeemUnderlying fails on dUSDC, it will
// succeed but nothing will happen except firing an ExternalError event. If
// the second part of the self-call (USDC transfer) fails, it will revert
// and roll back the first part of the call as well as fire an ExternalError
// event after returning from the failed call.
bytes memory returnData;
(ok, returnData) = address(this).call(abi.encodeWithSelector(
this._withdrawUSDCAtomic.selector, amount, recipient
));
if (!ok) {
// Find out why USDC transfer reverted (doesn't give revert reasons).
_diagnoseAndEmitUSDCSpecificError(_USDC.transfer.selector);
} else {
// Set ok to false if the call succeeded but the withdrawal failed.
ok = abi.decode(returnData, (bool));
}
}
/**
* @notice Protected function that can only be called from `withdrawUSDC` on
* this contract. It will attempt to withdraw the supplied amount of USDC, or
* the maximum amount if specified using `uint256(-1)`, to the supplied
* recipient address by redeeming the underlying USDC from the dUSDC contract
* and transferring it to the recipient. An ExternalError will be emitted and
* the transfer will be skipped if the call to `redeemUnderlying` fails, and
* any revert will be caught by `withdrawUSDC` and diagnosed in order to emit
* an appropriate ExternalError as well.
* @param amount uint256 The amount of USDC to withdraw.
* @param recipient address The account to transfer the withdrawn USDC to.
* @return True if the withdrawal succeeded, otherwise false.
*/
function _withdrawUSDCAtomic(
uint256 amount,
address recipient
) external returns (bool success) {
// Ensure caller is this contract and self-call context is correctly set.
_enforceSelfCallFrom(this.withdrawUSDC.selector);
// If amount = 0xfff...fff, withdraw the maximum amount possible.
bool maxWithdraw = (amount == uint256(-1));
if (maxWithdraw) {
// Attempt to redeem all dUSDC from Dharma USDC if there is a balance.
_withdrawMaxFromDharmaToken(AssetType.USDC);
// Then transfer all USDC to recipient if there is a balance.
require(_transferMax(_USDC, recipient, false));
success = true;
} else {
// Attempt to withdraw specified USDC from Dharma USDC before proceeding.
if (_withdrawFromDharmaToken(AssetType.USDC, amount)) {
// Ensure that the USDC transfer does not fail.
require(_USDC.transfer(recipient, amount));
success = true;
}
}
}
/**
* @notice Withdraw Ether to a provided recipient address by transferring it
* to a recipient. This is only intended to be utilized on V7 as a mechanism
* for recovering Ether from this contract.
* @param amount uint256 The amount of Ether to withdraw.
* @param recipient address The account to transfer the Ether to.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
* @return True if the transfer succeeded, otherwise false.
*/
function withdrawEther(
uint256 amount,
address payable recipient,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external returns (bool ok) {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.ETHWithdrawal,
abi.encode(amount, recipient),
minimumActionGas,
userSignature,
dharmaSignature
);
// Ensure that a non-zero amount of Ether has been supplied.
require(amount > 0, _revertReason(4));
// Ensure that a non-zero recipient has been supplied.
require(recipient != address(0), _revertReason(1));
// Attempt to transfer Ether to the recipient and emit an appropriate event.
ok = _transferETH(recipient, amount);
}
/**
* @notice Allow a signatory to increment the nonce at any point. The current
* nonce needs to be provided as an argument to the signature so as not to
* enable griefing attacks. All arguments can be omitted if called directly.
* No value is returned from this function - it will either succeed or revert.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param signature bytes A signature that resolves to either the public key
* set for this account in storage slot zero, `_userSigningKey`, or the public
* key returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
*/
function cancel(
uint256 minimumActionGas,
bytes calldata signature
) external {
// Get the current nonce.
uint256 nonceToCancel = _nonce;
// Ensure the caller or the supplied signature is valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.Cancel,
abi.encode(),
minimumActionGas,
signature,
signature
);
// Emit an event to validate that the nonce is no longer valid.
emit Cancel(nonceToCancel);
}
/**
* @notice Perform a generic call to another contract. Note that accounts with
* no code may not be specified, nor may the smart wallet itself or the escape
* hatch registry. In order to increment the nonce and invalidate the
* signatures, a call to this function with a valid target, signatutes, and
* gas will always succeed. To determine whether the call made as part of the
* action was successful or not, either the return values or the `CallSuccess`
* or `CallFailure` event can be used.
* @param to address The contract to call.
* @param data bytes The calldata to provide when making the call.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
* @return A boolean signifying the status of the call, as well as any data
* returned from the call.
*/
function executeAction(
address to,
bytes calldata data,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external returns (bool ok, bytes memory returnData) {
// Ensure that the `to` address is a contract and is not this contract.
_ensureValidGenericCallTarget(to);
// Ensure caller and/or supplied signatures are valid and increment nonce.
(bytes32 actionID, uint256 nonce) = _validateActionAndIncrementNonce(
ActionType.Generic,
abi.encode(to, data),
minimumActionGas,
userSignature,
dharmaSignature
);
// Note: from this point on, there are no reverts (apart from out-of-gas or
// call-depth-exceeded) originating from this action. However, the call
// itself may revert, in which case the function will return `false`, along
// with the revert reason encoded as bytes, and fire an CallFailure event.
// Perform the action via low-level call and set return values using result.
(ok, returnData) = to.call(data);
// Emit a CallSuccess or CallFailure event based on the outcome of the call.
if (ok) {
// Note: while the call succeeded, the action may still have "failed"
// (for example, successful calls to Compound can still return an error).
emit CallSuccess(actionID, false, nonce, to, data, returnData);
} else {
// Note: while the call failed, the nonce will still be incremented, which
// will invalidate all supplied signatures.
emit CallFailure(actionID, nonce, to, data, string(returnData));
}
}
/**
* @notice Allow signatory to set a new user signing key. The current nonce
* needs to be provided as an argument to the signature so as not to enable
* griefing attacks. No value is returned from this function - it will either
* succeed or revert.
* @param userSigningKey address The new user signing key to set on this smart
* wallet.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
*/
function setUserSigningKey(
address userSigningKey,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.SetUserSigningKey,
abi.encode(userSigningKey),
minimumActionGas,
userSignature,
dharmaSignature
);
// Set new user signing key on smart wallet and emit a corresponding event.
_setUserSigningKey(userSigningKey);
}
/**
* @notice Set a dedicated address as the "escape hatch" account. This account
* can then call `escape()` at any point to "sweep" the entire Dai, USDC,
* residual cDai, cUSDC, dDai, dUSDC, and Ether balance from the smart wallet.
* This function call will revert if the smart wallet has previously called
* `permanentlyDisableEscapeHatch` at any point and disabled the escape hatch.
* No value is returned from this function - it will either succeed or revert.
* @param account address The account to set as the escape hatch account.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
*/
function setEscapeHatch(
address account,
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.SetEscapeHatch,
abi.encode(account),
minimumActionGas,
userSignature,
dharmaSignature
);
// Ensure that an escape hatch account has been provided.
require(account != address(0), _revertReason(5));
// Set a new escape hatch for the smart wallet unless it has been disabled.
_ESCAPE_HATCH_REGISTRY.setEscapeHatch(account);
}
/**
* @notice Remove the "escape hatch" account if one is currently set. This
* function call will revert if the smart wallet has previously called
* `permanentlyDisableEscapeHatch` at any point and disabled the escape hatch.
* No value is returned from this function - it will either succeed or revert.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
*/
function removeEscapeHatch(
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.RemoveEscapeHatch,
abi.encode(),
minimumActionGas,
userSignature,
dharmaSignature
);
// Remove the escape hatch for the smart wallet if one is currently set.
_ESCAPE_HATCH_REGISTRY.removeEscapeHatch();
}
/**
* @notice Permanently disable the "escape hatch" mechanism for this smart
* wallet. This function call will revert if the smart wallet has already
* called `permanentlyDisableEscapeHatch` at any point in the past. No value
* is returned from this function - it will either succeed or revert.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @param userSignature bytes A signature that resolves to the public key
* set for this account in storage slot zero, `_userSigningKey`. If the user
* signing key is not a contract, ecrecover will be used; otherwise, ERC1271
* will be used. A unique hash returned from `getCustomActionID` is prefixed
* and hashed to create the message hash for the signature.
* @param dharmaSignature bytes A signature that resolves to the public key
* returned for this account from the Dharma Key Registry. A unique hash
* returned from `getCustomActionID` is prefixed and hashed to create the
* signed message.
*/
function permanentlyDisableEscapeHatch(
uint256 minimumActionGas,
bytes calldata userSignature,
bytes calldata dharmaSignature
) external {
// Ensure caller and/or supplied signatures are valid and increment nonce.
_validateActionAndIncrementNonce(
ActionType.DisableEscapeHatch,
abi.encode(),
minimumActionGas,
userSignature,
dharmaSignature
);
// Permanently disable the escape hatch mechanism for this smart wallet.
_ESCAPE_HATCH_REGISTRY.permanentlyDisableEscapeHatch();
}
/**
* @notice Allow the designated escape hatch account to "sweep" the entire
* Sai, Dai, USDC, residual dDai, dUSDC, cSai, cDai & cUSDC, and Ether balance
* from the smart wallet. The call will revert for any other caller, or if
* there is no escape hatch account on this smart wallet. First, an attempt
* will be made to redeem any dDai or dUSDC that is currently deposited in a
* dToken. Then, attempts will be made to transfer all Sai, Dai, USDC,
* residual cSai, cDai & cUSDC, and Ether to the escape hatch account. If any
* portion of this operation does not succeed, it will simply be skipped,
* allowing the rest of the operation to proceed. Finally, an `Escaped` event
* will be emitted. No value is returned from this function - it will either
* succeed or revert.
*/
function escape() external {
// Get the escape hatch account, if one exists, for this account.
(bool exists, address escapeHatch) = _ESCAPE_HATCH_REGISTRY.getEscapeHatch();
// Ensure that an escape hatch is currently set for this smart wallet.
require(exists, _revertReason(6));
// Ensure that the escape hatch account is the caller.
require(msg.sender == escapeHatch, _revertReason(7));
// Attempt to redeem all dDai for Dai on Dharma Dai.
_withdrawMaxFromDharmaToken(AssetType.DAI);
// Attempt to redeem all dUSDC for USDC on Dharma USDC.
_withdrawMaxFromDharmaToken(AssetType.USDC);
// Attempt to transfer the total Dai balance to the caller.
_transferMax(_DAI, msg.sender, true);
// Attempt to transfer the total USDC balance to the caller.
_transferMax(_USDC, msg.sender, true);
// Attempt to transfer any residual cSai to the caller.
_transferMax(IERC20(address(_CSAI)), msg.sender, true);
// Attempt to transfer any residual cDai to the caller.
_transferMax(IERC20(address(_CDAI)), msg.sender, true);
// Attempt to transfer any residual cUSDC to the caller.
_transferMax(IERC20(address(_CUSDC)), msg.sender, true);
// Attempt to transfer any residual dDai to the caller.
_transferMax(IERC20(address(_DDAI)), msg.sender, true);
// Attempt to transfer any residual dUSDC to the caller.
_transferMax(IERC20(address(_DUSDC)), msg.sender, true);
// Determine if there is Ether at this address that should be transferred.
uint256 balance = address(this).balance;
if (balance > 0) {
// Attempt to transfer any Ether to caller and emit an appropriate event.
_transferETH(msg.sender, balance);
}
// Emit an `Escaped` event.
emit Escaped();
}
/**
* @notice Allow the account recovery manager to set a new user signing key on
* the smart wallet. The call will revert for any other caller. The account
* recovery manager implements a set of controls around the process, including
* a timelock and an option to permanently opt out of account recover. No
* value is returned from this function - it will either succeed or revert.
* @param newUserSigningKey address The new user signing key to set on this
* smart wallet.
*/
function recover(address newUserSigningKey) external {
require(msg.sender == _ACCOUNT_RECOVERY_MANAGER, _revertReason(8));
// Increment nonce to prevent signature reuse should original key be reset.
_nonce++;
// Set up the user's new dharma key and emit a corresponding event.
_setUserSigningKey(newUserSigningKey);
}
/**
* @notice Convert all available Sai for Dai. If the conversion fails, or if
* the realized exchange rate is less than 1:1, the call will revert. Note
* that cSai is not included as part of this operation.
*/
function migrateSaiToDai() external {
// Swap the current Sai balance on this contract for Dai.
_swapSaiForDai(_SAI.balanceOf(address(this)));
}
/**
* @notice Redeem all available cSAI for Sai, swap that Sai for Dai, and use
* that Dai to mint dDai. If any step in the process fails, the call will
* revert and prior steps will be rolled back. Also note that existing Sai and
* Dai are not included as part of this operation.
*/
function migrateCSaiToDDai() external {
// Get the current cSai balance for this account.
uint256 redeemAmount = _CSAI.balanceOf(address(this));
// Only perform the call to redeem if there is a non-zero balance.
if (redeemAmount > 0) {
// Get the current Sai balance on this contract.
uint256 currentSaiBalance = _SAI.balanceOf(address(this));
// Redeem underlying balance from cSai and revert if unsuccessful.
require(_CSAI.redeem(redeemAmount) == _COMPOUND_SUCCESS, _revertReason(9));
// Calculate difference between pre-redeem and post-redeem Sai balances.
uint256 saiBalance = _SAI.balanceOf(address(this)) - currentSaiBalance;
// Swap any Sai for Dai and get the newly-swapped Dai balance.
uint256 daiBalance = _swapSaiForDai(saiBalance);
// If the dDai allowance is insufficient, set it before depositing.
if (_DAI.allowance(address(this), address(_DDAI)) < daiBalance) {
require(_DAI.approve(address(_DDAI), uint256(-1)), _revertReason(10));
}
// Deposit the new Dai balance on Dharma Dai.
_DDAI.mint(daiBalance);
}
}
/**
* @notice Redeem all available cDAI for Dai and use that Dai to mint dDai. If
* any step in the process fails, the call will revert and prior steps will be
* rolled back. Also note that existing Sai and Dai are not included as part
* of this operation.
*/
function migrateCDaiToDDai() external {
_migrateCTokenToDToken(AssetType.DAI);
}
/**
* @notice Redeem all available cUSDC for USDC and use that USDC to mint
* dUSDC. If any step in the process fails, the call will revert and prior
* steps will be rolled back. Also note that existing USDC is not included as
* part of this operation.
*/
function migrateCUSDCToDUSDC() external {
_migrateCTokenToDToken(AssetType.USDC);
}
/**
* @notice View function to retrieve the Dai and USDC balances held by the
* smart wallet, both directly and held in Dharma Dai and Dharma USD Coin, as
* well as the Ether balance (the underlying dEther balance will always return
* zero in this implementation, as there is no dEther yet).
* @return The Dai balance, the USDC balance, the Ether balance, the
* underlying Dai balance of the dDai balance, and the underlying USDC balance
* of the dUSDC balance (zero will always be returned as the underlying Ether
* balance of the dEther balance in this implementation).
*/
function getBalances() external view returns (
uint256 daiBalance,
uint256 usdcBalance,
uint256 etherBalance,
uint256 dDaiUnderlyingDaiBalance,
uint256 dUsdcUnderlyingUsdcBalance,
uint256 dEtherUnderlyingEtherBalance // always returns 0
) {
daiBalance = _DAI.balanceOf(address(this));
usdcBalance = _USDC.balanceOf(address(this));
etherBalance = address(this).balance;
dDaiUnderlyingDaiBalance = _DDAI.balanceOfUnderlying(address(this));
dUsdcUnderlyingUsdcBalance = _DUSDC.balanceOfUnderlying(address(this));
}
/**
* @notice View function for getting the current user signing key for the
* smart wallet.
* @return The current user signing key.
*/
function getUserSigningKey() external view returns (address userSigningKey) {
userSigningKey = _userSigningKey;
}
/**
* @notice View function for getting the current nonce of the smart wallet.
* This nonce is incremented whenever an action is taken that requires a
* signature and/or a specific caller.
* @return The current nonce.
*/
function getNonce() external view returns (uint256 nonce) {
nonce = _nonce;
}
/**
* @notice View function that, given an action type and arguments, will return
* the action ID or message hash that will need to be prefixed (according to
* EIP-191 0x45), hashed, and signed by both the user signing key and by the
* key returned for this smart wallet by the Dharma Key Registry in order to
* construct a valid signature for the corresponding action. Any nonce value
* may be supplied, which enables constructing valid message hashes for
* multiple future actions ahead of time.
* @param action uint8 The type of action, designated by it's index. Valid
* custom actions in V7 include Cancel (0), SetUserSigningKey (1),
* DAIWithdrawal (10), USDCWithdrawal (5), ETHWithdrawal (6),
* SetEscapeHatch (7), RemoveEscapeHatch (8), and DisableEscapeHatch (9).
* @param amount uint256 The amount to withdraw for Withdrawal actions. This
* value is ignored for non-withdrawal action types.
* @param recipient address The account to transfer withdrawn funds to or the
* new user signing key. This value is ignored for Cancel, RemoveEscapeHatch,
* and DisableEscapeHatch action types.
* @param minimumActionGas uint256 The minimum amount of gas that must be
* provided to this call - be aware that additional gas must still be included
* to account for the cost of overhead incurred up until the start of this
* function call.
* @return The action ID, which will need to be prefixed, hashed and signed in
* order to construct a valid signature.
*/
function getNextCustomActionID(
ActionType action,
uint256 amount,
address recipient,
uint256 minimumActionGas
) external view returns (bytes32 actionID) {
// Determine the actionID - this serves as a signature hash for an action.
actionID = _getActionID(
action,
_validateCustomActionTypeAndGetArguments(action, amount, recipient),
_nonce,
minimumActionGas,
_userSigningKey,
_getDharmaSigningKey()
);
}
/**
* @notice View function that, given an action type and arguments, will return
* the action ID or message hash that will need to be prefixed (according to
* EIP-191 0x45), hashed, and signed by both the user signing key and by the
* key returned for this smart wallet by the Dharma Key Registry in order to
* construct a valid signature for the corresponding action. The current nonce