Skip to content

Commit a397c62

Browse files
committed
Address Bailsec audit findings
1 parent 83d0959 commit a397c62

File tree

1 file changed

+114
-30
lines changed

1 file changed

+114
-30
lines changed

src/deployer/SafeGuard.sol

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ interface ISafeMinimal {
1717

1818
function nonce() external view returns (uint256);
1919

20+
function removeOwner(address prevOwner, address oldOwner, uint256 threshold) external;
21+
2022
function isOwner(address) external view returns (bool);
2123

24+
function getThreshold() external view returns (uint256);
25+
2226
function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory);
2327

2428
function approvedHashes(address owner, bytes32 txHash) external view returns (bool);
@@ -90,6 +94,23 @@ library SafeLib {
9094
return keccak256(txHashData);
9195
}
9296

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+
93114
function OWNER_COUNT_SLOT(ISafeMinimal) internal pure returns (uint256) {
94115
return 3;
95116
}
@@ -187,10 +208,13 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
187208
error NotQueued(bytes32 txHash);
188209
error LockedDown(address lockedDownBy);
189210
error NotLockedDown();
190-
error UnlockHashNotApproved(bytes32 txHash);
191211
error UnexpectedUpgrade(address newSingleton);
192212
error Reentrancy();
193213
error ModuleInstalled(address module);
214+
error NotEnoughOwners(uint256 ownerCount);
215+
error ThresholdTooLow(uint256 threshold);
216+
error NotUnanimous(bytes32 txHash);
217+
error TxHashNotApproved(bytes32 txHash);
194218

195219
mapping(bytes32 => uint256) public timelockEnd;
196220
address public lockedDownBy;
@@ -199,6 +223,8 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
199223
bool private _guardRemoved;
200224

201225
ISafeMinimal public constant safe = ISafeMinimal(0xf36b9f50E59870A24F42F9Ba43b2aD0A4b8f2F51);
226+
uint256 internal constant _MINIMUM_OWNERS = 3;
227+
uint256 internal constant _MINIMUM_THRESHOLD = 2;
202228

203229
address private constant _SINGLETON = 0xfb1bffC9d739B8D520DaF37dF666da4C687191EA;
204230
address private constant _SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7;
@@ -284,20 +310,14 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
284310
_;
285311
}
286312

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.
291316
if (!safe.approvedHashes(msg.sender, txHash)) {
292-
revert UnlockHashNotApproved(txHash);
317+
revert TxHashNotApproved(txHash);
293318
}
294319
}
295320

296-
modifier antiGriefing() {
297-
_requireApprovedUnlock();
298-
_;
299-
}
300-
301321
function checkTransaction(
302322
address to,
303323
uint256 value,
@@ -366,24 +386,25 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
366386
);
367387
bytes32 txHash = _safe.getTransactionHash(txHashData);
368388

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()) {
380395
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+
}
381402
}
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.
386403

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.
387408
uint256 _timelockEnd = timelockEnd[txHash];
388409
if (_timelockEnd == 0) {
389410
revert NotQueued(txHash);
@@ -452,6 +473,21 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
452473
revert GuardIsOwner();
453474
}
454475

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+
455491
// We do not revert if `_safe.getGuard()` returns a value other than `address(this)`. This
456492
// allows uninstallation of the guard (through the timelock, obviously) to later permit
457493
// upgrades to other singleton implementation contracts. However, we do set the
@@ -509,7 +545,7 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
509545
);
510546
}
511547

512-
function unlockTxHash() public view normalOperation returns (bytes32) {
548+
function unlockTxHash() public view returns (bytes32) {
513549
uint256 nonce = safe.nonce();
514550
return safe.getTransactionHash(
515551
address(this),
@@ -525,7 +561,45 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
525561
);
526562
}
527563

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+
529603
uint256 _timelockEnd = timelockEnd[txHash];
530604
if (_timelockEnd == 0) {
531605
revert NotQueued(txHash);
@@ -537,8 +611,18 @@ contract ZeroExSettlerDeployerSafeGuard is IGuard {
537611
emit SafeTransactionCanceled(txHash, msg.sender);
538612
}
539613

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);
542626
lockedDownBy = msg.sender;
543627
}
544628

0 commit comments

Comments
 (0)