@@ -17,8 +17,12 @@ interface ISafeMinimal {
17
17
18
18
function nonce () external view returns (uint256 );
19
19
20
+ function removeOwner (address prevOwner , address oldOwner , uint256 threshold ) external ;
21
+
20
22
function isOwner (address ) external view returns (bool );
21
23
24
+ function getThreshold () external view returns (uint256 );
25
+
22
26
function getStorageAt (uint256 offset , uint256 length ) external view returns (bytes memory );
23
27
24
28
function approvedHashes (address owner , bytes32 txHash ) external view returns (bool );
@@ -90,6 +94,23 @@ library SafeLib {
90
94
return keccak256 (txHashData);
91
95
}
92
96
97
+ function OWNERS_SLOT (ISafeMinimal) internal pure returns (uint256 ) {
98
+ return 2 ;
99
+ }
100
+
101
+ function getPrevOwner (ISafeMinimal safe , address owner ) internal view returns (address ) {
102
+ address cursor = address (1 );
103
+ while (true ) {
104
+ address nextOwner =
105
+ abi.decode (safe.getStorageAt (uint256 (keccak256 (abi.encode (cursor, OWNERS_SLOT (safe)))), 1 ), (address ));
106
+ if (nextOwner == owner) {
107
+ return cursor;
108
+ }
109
+ cursor = nextOwner;
110
+ }
111
+ revert (); // unreachable
112
+ }
113
+
93
114
function OWNER_COUNT_SLOT (ISafeMinimal) internal pure returns (uint256 ) {
94
115
return 3 ;
95
116
}
@@ -187,10 +208,13 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
187
208
error NotQueued (bytes32 txHash );
188
209
error LockedDown (address lockedDownBy );
189
210
error NotLockedDown ();
190
- error UnlockHashNotApproved (bytes32 txHash );
191
211
error UnexpectedUpgrade (address newSingleton );
192
212
error Reentrancy ();
193
213
error ModuleInstalled (address module );
214
+ error NotEnoughOwners (uint256 ownerCount );
215
+ error ThresholdTooLow (uint256 threshold );
216
+ error NotUnanimous (bytes32 txHash );
217
+ error TxHashNotApproved (bytes32 txHash );
194
218
195
219
mapping (bytes32 => uint256 ) public timelockEnd;
196
220
address public lockedDownBy;
@@ -199,6 +223,8 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
199
223
bool private _guardRemoved;
200
224
201
225
ISafeMinimal public constant safe = ISafeMinimal (0xf36b9f50E59870A24F42F9Ba43b2aD0A4b8f2F51 );
226
+ uint256 internal constant _MINIMUM_OWNERS = 3 ;
227
+ uint256 internal constant _MINIMUM_THRESHOLD = 2 ;
202
228
203
229
address private constant _SINGLETON = 0xfb1bffC9d739B8D520DaF37dF666da4C687191EA ;
204
230
address private constant _SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 ;
@@ -284,20 +310,14 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
284
310
_;
285
311
}
286
312
287
- function _requireApprovedUnlock () private view onlyOwner {
288
- // By requiring that the Safe owner has preapproved the `txHash` for the call to `unlock`,
289
- // we prevent a single rogue signer from bricking the Safe.
290
- bytes32 txHash = unlockTxHash ();
313
+ function _requirePreApproved (bytes32 txHash ) private view {
314
+ // By requiring that the Safe owner has preapproved the `txHash`, we prevent a single rogue
315
+ // signer from bricking the Safe.
291
316
if (! safe.approvedHashes (msg .sender , txHash)) {
292
- revert UnlockHashNotApproved (txHash);
317
+ revert TxHashNotApproved (txHash);
293
318
}
294
319
}
295
320
296
- modifier antiGriefing () {
297
- _requireApprovedUnlock ();
298
- _;
299
- }
300
-
301
321
function checkTransaction (
302
322
address to ,
303
323
uint256 value ,
@@ -366,24 +386,25 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
366
386
);
367
387
bytes32 txHash = _safe.getTransactionHash (txHashData);
368
388
369
- // The call to `this.unlock()` is special-cased.
370
- if (to == address (this ) && uint256 (uint32 (bytes4 (data))) == uint256 (uint32 (this .unlock.selector ))) {
371
- // A call to `unlock` does not go through the timelock, but we require additional
372
- // signatures in order for the lockdown functionality to be effective, as a protocol.
373
-
374
- // Calling `unlock()` requires unanimous signatures, i.e. a threshold equal to the owner
375
- // count. We go beyond the usual requirement of just the threshold. The owner who called
376
- // `lockDown()` has already signed (to prevent griefing).
377
- uint256 ownerCount = _safe.ownerCount ();
378
- _safe.checkNSignatures (txHash, txHashData, signatures, ownerCount);
379
-
389
+ // Any transaction with unanimous signatures can bypass the timelock. This mechanism is also
390
+ // critical to the anti-griefing provisions. The pre-signed transaction(s) required when
391
+ // calling `lockDown()` or `cancel(...)` can be combined with signatures from well-behaved
392
+ // keyholders to un-brick the safe and remove the misbehaving actors. Unanimous transactions
393
+ // also cannot be `cancel(...)`'d.
394
+ try _safe.checkNSignatures (txHash, txHashData, signatures, _safe.ownerCount ()) {
380
395
return ;
396
+ } catch {
397
+ // The signatures are not unanimous; proceed to the timelock. If the call is to
398
+ // `unlock()`, we bail out because it *MUST* be unanimous.
399
+ if (to == address (this ) && uint256 (uint32 (bytes4 (data))) == uint256 (uint32 (this .unlock.selector ))) {
400
+ revert NotUnanimous (txHash);
401
+ }
381
402
}
382
- // Fall through to the "normal" case, where we're doing anything except calling
383
- // `this.unlock()`. The checks that need to be performed here are 1) that the Safe is not
384
- // locked down, 2) that the transaction was previously queued through `enqueue` and 3) that
385
- // `delay` has elapsed since `enqueue` was called.
386
403
404
+ // Fall through to the "normal" case. The checks that need to be performed here are 1) that
405
+ // the Safe is not locked down (checked in `checkAfterExecution`), 2) that the transaction
406
+ // was previously queued through `enqueue` and 3) that `delay` has elapsed since `enqueue`
407
+ // was called.
387
408
uint256 _timelockEnd = timelockEnd[txHash];
388
409
if (_timelockEnd == 0 ) {
389
410
revert NotQueued (txHash);
@@ -452,6 +473,21 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
452
473
revert GuardIsOwner ();
453
474
}
454
475
476
+ // Some basic safety checks. If violated, the game theory of the `lockDown`/`unlock` game
477
+ // becomes degenerate.
478
+ {
479
+ uint256 ownerCount = _safe.ownerCount ();
480
+ if (ownerCount < _MINIMUM_OWNERS) {
481
+ revert NotEnoughOwners (ownerCount);
482
+ }
483
+ }
484
+ {
485
+ uint256 threshold = _safe.getThreshold ();
486
+ if (threshold < _MINIMUM_THRESHOLD) {
487
+ revert ThresholdTooLow (threshold);
488
+ }
489
+ }
490
+
455
491
// We do not revert if `_safe.getGuard()` returns a value other than `address(this)`. This
456
492
// allows uninstallation of the guard (through the timelock, obviously) to later permit
457
493
// upgrades to other singleton implementation contracts. However, we do set the
@@ -509,7 +545,7 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
509
545
);
510
546
}
511
547
512
- function unlockTxHash () public view normalOperation returns (bytes32 ) {
548
+ function unlockTxHash () public view returns (bytes32 ) {
513
549
uint256 nonce = safe.nonce ();
514
550
return safe.getTransactionHash (
515
551
address (this ),
@@ -525,7 +561,45 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
525
561
);
526
562
}
527
563
528
- function cancel (bytes32 txHash ) external antiGriefing {
564
+ function _removeOwnerTxHash (address prevOwner , address oldOwner , uint256 threshold , uint256 nonce )
565
+ private
566
+ view
567
+ returns (bytes32 )
568
+ {
569
+ return safe.getTransactionHash (
570
+ address (safe),
571
+ 0 ether,
572
+ abi.encodeCall (safe.removeOwner, (prevOwner, oldOwner, threshold)),
573
+ Operation.Call,
574
+ 0 ,
575
+ 0 ,
576
+ 0 ,
577
+ address (0 ),
578
+ payable (address (0 )),
579
+ nonce
580
+ );
581
+ }
582
+
583
+ function resignTxHash (address owner ) external view returns (bytes32 txHash ) {
584
+ address prevOwner = safe.getPrevOwner (owner);
585
+ uint256 threshold = safe.getThreshold ();
586
+ uint256 nonce = safe.nonce ();
587
+ if (
588
+ lockedDownBy != address (0 )
589
+ || safe.approvedHashes (owner, txHash = _removeOwnerTxHash (prevOwner, owner, threshold, nonce))
590
+ ) {
591
+ nonce++ ;
592
+ txHash = _removeOwnerTxHash (prevOwner, owner, threshold, nonce);
593
+ }
594
+ }
595
+
596
+ function cancel (bytes32 txHash ) external onlyOwner {
597
+ uint256 nonce = safe.nonce ();
598
+ if (lockedDownBy != address (0 )) {
599
+ nonce++ ;
600
+ }
601
+ _requirePreApproved (_removeOwnerTxHash (safe.getPrevOwner (msg .sender ), msg .sender , safe.getThreshold (), nonce));
602
+
529
603
uint256 _timelockEnd = timelockEnd[txHash];
530
604
if (_timelockEnd == 0 ) {
531
605
revert NotQueued (txHash);
@@ -537,8 +611,18 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
537
611
emit SafeTransactionCanceled (txHash, msg .sender );
538
612
}
539
613
540
- function lockDown () external normalOperation antiGriefing {
541
- emit LockDown (msg .sender , unlockTxHash ());
614
+ function lockDown () external normalOperation onlyOwner {
615
+ address prevOwner = safe.getPrevOwner (msg .sender );
616
+ uint256 threshold = safe.getThreshold ();
617
+ uint256 nonce = safe.nonce ();
618
+ if (safe.approvedHashes (msg .sender , _removeOwnerTxHash (prevOwner, msg .sender , threshold, nonce))) {
619
+ nonce++ ;
620
+ _requirePreApproved (_removeOwnerTxHash (prevOwner, msg .sender , threshold, nonce));
621
+ }
622
+ bytes32 txHash = unlockTxHash ();
623
+ _requirePreApproved (txHash);
624
+
625
+ emit LockDown (msg .sender , txHash);
542
626
lockedDownBy = msg .sender ;
543
627
}
544
628
0 commit comments