Commit 927bcaaa authored by Maurelian's avatar Maurelian

fix(ctb): Fix off-by-one in removeOwner

There was a bug in the remove all owners flow that would leave two owners on the safe.
Fixing that bug required using `swapOwners` in order to remove and
replace the last signer, since you cannot remove the only owner and then
add a new one (even when calling from a module).
parent a5a723d3
...@@ -88,29 +88,32 @@ contract LivenessModule is ISemver { ...@@ -88,29 +88,32 @@ contract LivenessModule is ISemver {
// Remove owners one at a time starting from the last owner. // Remove owners one at a time starting from the last owner.
// Since we're removing them in order from last to first, the ordering will remain constant, // Since we're removing them in order from last to first, the ordering will remain constant,
// and we shouldn't need to query the list of owners again. // and we shouldn't need to query the list of owners again.
for (uint256 i = owners.length - 1; i >= 0; i--) { address prevOwner;
for (uint256 i = owners.length - 1; i > 0; i--) {
address currentOwner = owners[i]; address currentOwner = owners[i];
address prevOwner = _getPrevOwner(currentOwner, owners); prevOwner = _getPrevOwner(currentOwner, owners);
if (currentOwner != address(this)) {
// Call the Safe to remove the owner // Call the Safe to remove the owner
_removeOwner({ _prevOwner: prevOwner, _owner: currentOwner, _threshold: 1 }); _removeOwner({ _prevOwner: prevOwner, _owner: currentOwner, _threshold: 1 });
}
} }
prevOwner = _getPrevOwner(owners[0], owners);
// Add the fallback owner as the sole owner of the Safe // Add the fallback owner as the sole owner of the Safe
_giveToFallbackOwner(); _swapToFallbackOwner({ _prevOwner: prevOwner, _oldOwner: owners[0] });
} }
_verifyFinalState(); _verifyFinalState();
} }
/// @notice Sets the fallback owner as the sole owner of the Safe with a threshold of 1 /// @notice Sets the fallback owner as the sole owner of the Safe with a threshold of 1
function _giveToFallbackOwner() internal { /// @param _prevOwner Owner that pointed to the owner to be replaced in the linked list
/// @param _oldOwner Owner address to be replaced.
function _swapToFallbackOwner(address _prevOwner, address _oldOwner) internal {
SAFE.execTransactionFromModule({ SAFE.execTransactionFromModule({
to: address(SAFE), to: address(SAFE),
value: 0, value: 0,
operation: Enum.Operation.Call, operation: Enum.Operation.Call,
data: abi.encodeCall(OwnerManager.addOwnerWithThreshold, (FALLBACK_OWNER, 1)) data: abi.encodeCall(OwnerManager.swapOwner, (_prevOwner, _oldOwner, FALLBACK_OWNER))
}); });
} }
...@@ -157,14 +160,16 @@ contract LivenessModule is ISemver { ...@@ -157,14 +160,16 @@ contract LivenessModule is ISemver {
} }
/// @notice Get the previous owner in the linked list of owners /// @notice Get the previous owner in the linked list of owners
function _getPrevOwner(address owner, address[] memory owners) internal pure returns (address prevOwner_) { /// @param _owner The owner whose previous owner we want to find
for (uint256 i = 0; i < owners.length; i++) { /// @param _owners The list of owners
if (owners[i] != owner) continue; function _getPrevOwner(address _owner, address[] memory _owners) internal pure returns (address prevOwner_) {
for (uint256 i = 0; i < _owners.length; i++) {
if (_owners[i] != _owner) continue;
if (i == 0) { if (i == 0) {
prevOwner_ = SENTINEL_OWNERS; prevOwner_ = SENTINEL_OWNERS;
break; break;
} }
prevOwner_ = owners[i - 1]; prevOwner_ = _owners[i - 1];
} }
} }
......
...@@ -86,9 +86,17 @@ contract LivenessModule_RemoveOwner_Test is LivenessModule_TestInit { ...@@ -86,9 +86,17 @@ contract LivenessModule_RemoveOwner_Test is LivenessModule_TestInit {
function test_removeOwner_allOwners_succeeds() external { function test_removeOwner_allOwners_succeeds() external {
vm.warp(block.timestamp + 30 days); vm.warp(block.timestamp + 30 days);
// The safe is initialized with 10 owners, so we need to remove 3 to get below the minOwners threshold uint256 numOwners = safeInstance.owners.length;
livenessModule.removeOwner(safeInstance.owners[0]); uint256 minOwners = livenessModule.minOwners();
livenessModule.removeOwner(safeInstance.owners[1]);
livenessModule.removeOwner(safeInstance.owners[2]); // Remove enough owners to trigger the transfer to the fallbackOwner
uint256 numToRemove = numOwners - minOwners + 1;
for (uint256 i = 0; i < numToRemove; i++) {
livenessModule.removeOwner(safeInstance.owners[i]);
}
assertEq(safeInstance.safe.getOwners().length, 1);
assertEq(safeInstance.safe.getOwners()[0], fallbackOwner);
assertEq(safeInstance.safe.getThreshold(), 1);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment