Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
a56e6c75
Unverified
Commit
a56e6c75
authored
Oct 04, 2023
by
Maurelian
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ctb): Add liveness module
parent
fa0db2a6
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
190 additions
and
4 deletions
+190
-4
LivenessGuard.sol
packages/contracts-bedrock/src/Safe/LivenessGuard.sol
+15
-4
LivenessModule.sol
packages/contracts-bedrock/src/Safe/LivenessModule.sol
+124
-0
LivenessModule.t.sol
packages/contracts-bedrock/test/LivenessModule.t.sol
+51
-0
No files found.
packages/contracts-bedrock/src/Safe/LivenessGuard.sol
View file @
a56e6c75
...
@@ -2,7 +2,8 @@
...
@@ -2,7 +2,8 @@
pragma solidity 0.8.15;
pragma solidity 0.8.15;
import { Safe } from "safe-contracts/Safe.sol";
import { Safe } from "safe-contracts/Safe.sol";
import { BaseGuard } from "safe-contracts/base/GuardManager.sol";
import { BaseGuard, GuardManager } from "safe-contracts/base/GuardManager.sol";
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
import { SignatureDecoder } from "safe-contracts/common/SignatureDecoder.sol";
import { SignatureDecoder } from "safe-contracts/common/SignatureDecoder.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
...
@@ -40,9 +41,19 @@ contract LivenessGuard is SignatureDecoder, BaseGuard {
...
@@ -40,9 +41,19 @@ contract LivenessGuard is SignatureDecoder, BaseGuard {
external
external
{
{
require(msg.sender == address(safe), "LivenessGuard: only Safe can call this function");
require(msg.sender == address(safe), "LivenessGuard: only Safe can call this function");
if (to == address(safe) && data[0:4] == bytes4(keccak256("setGuard(address)"))) {
// We can't allow the guard to be disabled, or else the upgrade delay can be bypassed.
// There are a number of ways in which we need to constrain this safe so that it cannot remove
// TODO(Maurelian): Figure out how to best address this.
// this guard, nor the LivenessModule.
// TODO(Maurelian): Figure out how to best address this. The following is just intended to outline the
// known mathods by which a Safe could remove the liveness checks.
// TODO(Maurelian): Do we _need_ to have this feature at all?
bytes4 dataSig = bytes4(data);
if (
to == address(safe)
&& (dataSig == GuardManager.setGuard.selector || dataSig == ModuleManager.enableModule.selector)
|| operation == Enum.Operation.DelegateCall
) {
revert("LivenessGuard: cannot remove LivenessGuard or LivenessModule");
}
}
// This call will reenter to the Safe which is calling it. This is OK because it is only reading the
// This call will reenter to the Safe which is calling it. This is OK because it is only reading the
...
...
packages/contracts-bedrock/src/Safe/LivenessModule.sol
View file @
a56e6c75
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Safe } from "safe-contracts/Safe.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import { OwnerManager } from "safe-contracts/base/OwnerManager.sol";
import { LivenessGuard } from "src/Safe/LivenessGuard.sol";
/// @title LivenessModule
/// @notice This module is intended to be used in conjunction with the LivenessGuard. It should be able to
/// execute a transaction on the Safe in only a small number of cases:
contract LivenessModule {
/// @notice The Safe contract instance
Safe public safe;
/// @notice The LivenessGuard contract instance
LivenessGuard public livenessGuard;
/// @notice The interval, in seconds, during which an owner must have demonstrated liveness
uint256 public livenessInterval;
/// @notice The minimum number of owners before ownership of the safe is transferred to the fallback owner.
uint256 public minOwners;
/// @notice The fallback owner of the Safe
address public fallbackOwner;
// Constructor to initialize the Safe and baseModule instances
constructor(
Safe _safe,
LivenessGuard _livenessGuard,
uint256 _livenessInterval,
uint256 _minOwners,
address _fallbackOwner
) {
safe = _safe;
livenessGuard = _livenessGuard;
livenessInterval = _livenessInterval;
minOwners = _minOwners;
fallbackOwner = _fallbackOwner;
}
/// @notice This function can be called by anyone to remove an owner that has not signed a transaction
/// during the livness interval. If the number of owners drops below
function removeOwner(address owner) external {
// Check that the owner has not signed a transaction in the last 30 days
require(
livenessGuard.lastSigned(owner) < block.timestamp - livenessInterval,
"LivenessModule: owner has signed recently"
);
// Calculate the new threshold
uint256 numOwnersAfter = safe.getOwners().length - 1;
uint256 thresholdAfter = get75PercentThreshold(numOwnersAfter);
if (numOwnersAfter >= 8) {
safe.execTransactionFromModule({
to: address(safe),
value: 0,
data: abi.encodeCall(
// Call the Safe to remove the owner
OwnerManager.removeOwner,
(getPrevOwner(owner), owner, thresholdAfter)
),
operation: Enum.Operation.Call
});
} else {
// The number of owners is dangerously low, so we wish to transfer the ownership of this Safe to a new
// to the fallback owner.
// Remove owners one at a time starting from the last owner.
// Since we're removing them in order, the ordering will remain constant,
// and we shouldn't need to query the list of owners again.
address[] memory owners = safe.getOwners();
for (uint256 i = owners.length - 1; i >= 0; i--) {
address currentOwner = owners[i];
if (currentOwner != address(this)) {
safe.execTransactionFromModule({
to: address(safe),
value: 0,
data: abi.encodeCall(
// Call the Safe to remove the owner
OwnerManager.removeOwner,
(
getPrevOwner(currentOwner),
currentOwner,
1 // The threshold is 1 because we are removing all owners except the fallback owner
)
),
operation: Enum.Operation.Call
});
}
}
// Add the fallback owner as the sole owner of the Safe
safe.execTransactionFromModule({
to: address(safe),
value: 0,
data: abi.encodeCall(OwnerManager.addOwnerWithThreshold, (fallbackOwner, 1)),
operation: Enum.Operation.Call
});
address[] memory ownersAfter = safe.getOwners();
require(
ownersAfter.length == 1 && ownersAfter[0] == fallbackOwner,
"LivenessModule: fallback owner was not added as the sole owner"
);
}
}
/// @notice Get the previous owner in the linked list of owners
function getPrevOwner(address owner) public view returns (address prevOwner_) {
address[] memory owners = safe.getOwners();
prevOwner_ = address(0);
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == owner) {
prevOwner_ = owners[i - 1];
break;
}
}
}
/// @notice For a given number of owners, return the lowest threshold which is greater than 75.
function get75PercentThreshold(uint256 _numOwners) public view returns (uint256 threshold_) {
threshold_ = (_numOwners * 75 + 99) / 100;
}
}
packages/contracts-bedrock/test/LivenessModule.t.sol
0 → 100644
View file @
a56e6c75
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Test, StdUtils } from "forge-std/Test.sol";
import { Safe } from "safe-contracts/Safe.sol";
import { SafeProxyFactory } from "safe-contracts/proxies/SafeProxyFactory.sol";
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
import { Enum } from "safe-contracts/common/Enum.sol";
import "test/safe-tools/SafeTestTools.sol";
import { LivenessModule } from "src/Safe/LivenessModule.sol";
import { LivenessGuard } from "src/Safe/LivenessGuard.sol";
contract LivnessModule_TestInit is Test, SafeTestTools {
using SafeTestLib for SafeInstance;
event SignersRecorded(bytes32 indexed txHash, address[] signers);
LivenessModule livenessModule;
LivenessGuard livenessGuard;
SafeInstance safeInstance;
function makeKeys(uint256 num) public pure returns (uint256[] memory) {
uint256[] memory keys = new uint256[](num);
for (uint256 i; i < num; i++) {
keys[i] = uint256(keccak256(abi.encodePacked(i)));
}
}
function setUp() public {
// Create a Safe with 10 owners
uint256[] memory keys = makeKeys(10);
safeInstance = _setupSafe(keys, 8);
livenessGuard = new LivenessGuard(safeInstance.safe);
livenessModule = new LivenessModule({
_safe: safeInstance.safe,
_livenessGuard: livenessGuard,
_livenessInterval: 30 days,
_minOwners: 6,
_fallbackOwner: makeAddr("fallbackOwner")
});
safeInstance.enableModule(address(livenessModule));
}
}
contract LivenessModule_RemoveOwner_Test is LivnessModule_TestInit {
function test_removeOwner_succeeds() external {
vm.warp(block.timestamp + 30 days);
livenessModule.removeOwner(safeInstance.owners[0]);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment