Commit 67a17c31 authored by Maurelian's avatar Maurelian

refactor(ctb): Change to removeOwners

This commit introduces the ability to remove multiple owners at once in the LivenessModule.sol contract. A new function removeOwners has been added, which accepts arrays of previous owners and owners to be removed. The existing removeOwner function has been updated to use a new internal function _removeOwner. The _getPrevOwner function has been removed as it is no longer needed.
parent 432f22c2
...@@ -37,9 +37,6 @@ contract LivenessModule is ISemver { ...@@ -37,9 +37,6 @@ contract LivenessModule is ISemver {
/// keccak256("guard_manager.guard.address") /// keccak256("guard_manager.guard.address")
uint256 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8; uint256 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
/// @notice The address of the first owner in the linked list of owners
address internal constant SENTINEL_OWNERS = address(0x1);
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 1.0.0 /// @custom:semver 1.0.0
string public constant version = "1.0.0"; string public constant version = "1.0.0";
...@@ -62,71 +59,73 @@ contract LivenessModule is ISemver { ...@@ -62,71 +59,73 @@ contract LivenessModule is ISemver {
); );
} }
function removeOwners(address[] memory _previousOwners, address[] memory _ownersToRemove) external {
require(_previousOwners.length == _ownersToRemove.length, "LivenessModule: arrays must be the same length");
for (uint256 i = 0; i < _previousOwners.length; i++) {
_removeOwner(_previousOwners[i], _ownersToRemove[i]);
}
_verifyFinalState();
}
/// @notice This function can be called by anyone to remove an owner that has not signed a transaction /// @notice This function can be called by anyone to remove an owner that has not signed a transaction
/// during the liveness interval. If the number of owners drops below the minimum, then the /// during the liveness interval. If the number of owners drops below the minimum, then the
/// ownership of the Safe is transferred to the fallback owner. /// ownership of the Safe is transferred to the fallback owner.
function removeOwner(address owner) external { function _removeOwner(address _prevOwner, address _ownerToRemove) internal {
// Check that the owner to remove has not signed a transaction in the last 30 days
require(
LIVENESS_GUARD.lastLive(owner) < block.timestamp - LIVENESS_INTERVAL,
"LivenessModule: owner has signed recently"
);
// Calculate the new threshold // Calculate the new threshold
address[] memory owners = SAFE.getOwners(); address[] memory owners = SAFE.getOwners();
uint256 numOwnersAfter = owners.length - 1; uint256 numOwnersAfter = owners.length - 1;
if (_isAboveMinOwners(numOwnersAfter)) { if (_isAboveMinOwners(numOwnersAfter)) {
// Check that the owner to remove has not signed a transaction in the last 30 days
require(
LIVENESS_GUARD.lastLive(_ownerToRemove) < block.timestamp - LIVENESS_INTERVAL,
"LivenessModule: owner has signed recently"
);
// Call the Safe to remove the owner and update the threshold // Call the Safe to remove the owner and update the threshold
uint256 thresholdAfter = get75PercentThreshold(numOwnersAfter); uint256 thresholdAfter = get75PercentThreshold(numOwnersAfter);
address prevOwner = _getPrevOwner(owner, owners); _removeOwnerSafeCall({ _prevOwner: _prevOwner, _owner: _ownerToRemove, _threshold: thresholdAfter });
_removeOwner({ _prevOwner: prevOwner, _owner: owner, _threshold: thresholdAfter });
} else { } else {
// The number of owners is dangerously low, so we wish to transfer the ownership of this Safe // The number of owners is dangerously low, so we wish to transfer the ownership of this Safe
// to the fallback owner. // to the fallback owner. Therefore we no longer need to validate the liveness of the owners
// before removing them.
// Remove owners one at a time starting from the last owner. if (numOwnersAfter == 0) {
// 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.
address prevOwner;
for (uint256 i = owners.length - 1; i > 0; i--) {
address currentOwner = owners[i];
prevOwner = _getPrevOwner(currentOwner, owners);
// Call the Safe to remove the owner
_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
_swapToFallbackOwner({ _prevOwner: prevOwner, _oldOwner: owners[0] }); _swapToFallbackOwnerSafeCall({ _prevOwner: _prevOwner, _oldOwner: _ownerToRemove });
} else {
// Remove the owner and set the threshold to 1
_removeOwnerSafeCall({ _prevOwner: _prevOwner, _owner: _ownerToRemove, _threshold: 1 });
}
} }
_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
/// @param _prevOwner Owner that pointed to the owner to be replaced in the linked list /// @param _prevOwner Owner that pointed to the owner to be replaced in the linked list
/// @param _oldOwner Owner address to be replaced. /// @param _oldOwner Owner address to be replaced.
function _swapToFallbackOwner(address _prevOwner, address _oldOwner) internal { function _swapToFallbackOwnerSafeCall(address _prevOwner, address _oldOwner) internal {
require(
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.swapOwner, (_prevOwner, _oldOwner, FALLBACK_OWNER)) data: abi.encodeCall(OwnerManager.swapOwner, (_prevOwner, _oldOwner, FALLBACK_OWNER))
}); }),
"LivenessModule: failed to swap to fallback owner"
);
} }
/// @notice Removes the owner `owner` from the Safe and updates the threshold to `_threshold`. /// @notice Removes the owner `owner` from the Safe and updates the threshold to `_threshold`.
/// @param _prevOwner Owner that pointed to the owner to be removed in the linked list /// @param _prevOwner Owner that pointed to the owner to be removed in the linked list
/// @param _owner Owner address to be removed. /// @param _owner Owner address to be removed.
/// @param _threshold New threshold. /// @param _threshold New threshold.
function _removeOwner(address _prevOwner, address _owner, uint256 _threshold) internal { function _removeOwnerSafeCall(address _prevOwner, address _owner, uint256 _threshold) internal {
require(
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.removeOwner, (_prevOwner, _owner, _threshold)) data: abi.encodeCall(OwnerManager.removeOwner, (_prevOwner, _owner, _threshold))
}); }),
"LivenessModule: failed to remove owner"
);
} }
/// @notice A FREI-PI invariant check enforcing requirements on number of owners and threshold. /// @notice A FREI-PI invariant check enforcing requirements on number of owners and threshold.
...@@ -158,20 +157,6 @@ contract LivenessModule is ISemver { ...@@ -158,20 +157,6 @@ contract LivenessModule is ISemver {
); );
} }
/// @notice Get the previous owner in the linked list of owners
/// @param _owner The owner whose previous owner we want to find
/// @param _owners The list of owners
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) {
prevOwner_ = SENTINEL_OWNERS;
break;
}
prevOwner_ = _owners[i - 1];
}
}
/// @notice For a given number of owners, return the lowest threshold which is greater than 75. /// @notice For a given number of owners, return the lowest threshold which is greater than 75.
/// Note: this function returns 1 for numOwners == 1. /// Note: this function returns 1 for numOwners == 1.
function get75PercentThreshold(uint256 _numOwners) public pure returns (uint256 threshold_) { function get75PercentThreshold(uint256 _numOwners) public pure returns (uint256 threshold_) {
......
...@@ -5,26 +5,57 @@ import { Test, StdUtils } from "forge-std/Test.sol"; ...@@ -5,26 +5,57 @@ import { Test, StdUtils } from "forge-std/Test.sol";
import { Safe } from "safe-contracts/Safe.sol"; import { Safe } from "safe-contracts/Safe.sol";
import { SafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol"; import { SafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol";
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol"; import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
import { OwnerManager } from "safe-contracts/base/OwnerManager.sol";
import { Enum } from "safe-contracts/common/Enum.sol"; import { Enum } from "safe-contracts/common/Enum.sol";
import "test/safe-tools/SafeTestTools.sol"; import "test/safe-tools/SafeTestTools.sol";
import { LivenessModule } from "src/Safe/LivenessModule.sol"; import { LivenessModule } from "src/Safe/LivenessModule.sol";
import { LivenessGuard } from "src/Safe/LivenessGuard.sol"; import { LivenessGuard } from "src/Safe/LivenessGuard.sol";
contract OwnerSimulator is OwnerManager {
constructor(address[] memory _owners, uint256 _threshold) {
setupOwners(_owners, _threshold);
}
function removeOwnerWrapped(address prevOwner, address owner, uint256 _threshold) public {
OwnerManager(address(this)).removeOwner(prevOwner, owner, _threshold);
}
}
contract LivenessModule_TestInit is Test, SafeTestTools { contract LivenessModule_TestInit is Test, SafeTestTools {
using SafeTestLib for SafeInstance; using SafeTestLib for SafeInstance;
/// @notice The address of the first owner in the linked list of owners
address internal constant SENTINEL_OWNERS = address(0x1);
event SignersRecorded(bytes32 indexed txHash, address[] signers); event SignersRecorded(bytes32 indexed txHash, address[] signers);
LivenessModule livenessModule; LivenessModule livenessModule;
LivenessGuard livenessGuard; LivenessGuard livenessGuard;
SafeInstance safeInstance; SafeInstance safeInstance;
OwnerSimulator ownerSimulator;
address fallbackOwner; address fallbackOwner;
/// @notice Get the previous owner in the linked list of owners
/// @param _owner The owner whose previous owner we want to find
/// @param _owners The list of owners
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) {
prevOwner_ = SENTINEL_OWNERS;
break;
}
prevOwner_ = _owners[i - 1];
}
}
function setUp() public { function setUp() public {
// Create a Safe with 10 owners // Create a Safe with 10 owners
(, uint256[] memory keys) = makeAddrsAndKeys(10); (, uint256[] memory keys) = makeAddrsAndKeys(10);
safeInstance = _setupSafe(keys, 8); safeInstance = _setupSafe(keys, 8);
ownerSimulator = new OwnerSimulator(safeInstance.owners, 1);
livenessGuard = new LivenessGuard(safeInstance.safe); livenessGuard = new LivenessGuard(safeInstance.safe);
fallbackOwner = makeAddr("fallbackOwner"); fallbackOwner = makeAddr("fallbackOwner");
livenessModule = new LivenessModule({ livenessModule = new LivenessModule({
...@@ -79,22 +110,34 @@ contract LivenessModule_Get75PercentThreshold_Test is LivenessModule_TestInit { ...@@ -79,22 +110,34 @@ contract LivenessModule_Get75PercentThreshold_Test is LivenessModule_TestInit {
contract LivenessModule_RemoveOwner_Test is LivenessModule_TestInit { contract LivenessModule_RemoveOwner_Test is LivenessModule_TestInit {
function test_removeOwner_oneOwner_succeeds() external { function test_removeOwner_oneOwner_succeeds() external {
uint256 ownersBefore = safeInstance.owners.length; uint256 ownersBefore = safeInstance.owners.length;
address[] memory prevOwners = new address[](1);
address[] memory ownersToRemove = new address[](1);
ownersToRemove[0] = safeInstance.owners[0];
prevOwners[0] = _getPrevOwner(safeInstance.owners[0], safeInstance.owners);
address ownerToRemove = safeInstance.owners[0];
vm.warp(block.timestamp + 30 days); vm.warp(block.timestamp + 30 days);
livenessModule.removeOwner(safeInstance.owners[0]);
livenessModule.removeOwners(prevOwners, ownersToRemove);
assertEq(safeInstance.safe.getOwners().length, ownersBefore - 1); assertEq(safeInstance.safe.getOwners().length, ownersBefore - 1);
} }
function test_removeOwner_allOwners_succeeds() external { function test_removeOwner_allOwners_succeeds() external {
vm.warp(block.timestamp + 30 days); vm.warp(block.timestamp + 30 days);
uint256 numOwners = safeInstance.owners.length; uint256 numOwners = safeInstance.owners.length;
uint256 minOwners = livenessModule.minOwners();
// Remove enough owners to trigger the transfer to the fallbackOwner
uint256 numToRemove = numOwners - minOwners + 1;
for (uint256 i = 0; i < numToRemove; i++) { address[] memory prevOwners = new address[](numOwners);
livenessModule.removeOwner(safeInstance.owners[i]); address[] memory ownersToRemove = new address[](numOwners);
address[] memory currentOwners;
for (uint256 i = 0; i < numOwners; i++) {
currentOwners = ownerSimulator.getOwners();
ownersToRemove[i] = safeInstance.owners[i];
prevOwners[i] = _getPrevOwner(safeInstance.owners[i], currentOwners);
if (currentOwners.length == 1) break;
ownerSimulator.removeOwnerWrapped(prevOwners[i], ownersToRemove[i], 1);
} }
livenessModule.removeOwners(prevOwners, ownersToRemove);
assertEq(safeInstance.safe.getOwners().length, 1); assertEq(safeInstance.safe.getOwners().length, 1);
assertEq(safeInstance.safe.getOwners()[0], fallbackOwner); assertEq(safeInstance.safe.getOwners()[0], fallbackOwner);
assertEq(safeInstance.safe.getThreshold(), 1); 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