Skip to content

Commit c9d5e95

Browse files
committed
Compose TimelockGuard and LivenessModule2 into a single contract
Also makes the parent contracts abstract in order to prevent deploying them individually. The benefit of this approach is that it ensures the logic is available in both contracts, while ensuring that their state and logic are kept separate thus reducing the complexity and review effort.
1 parent f650849 commit c9d5e95

File tree

6 files changed

+55
-23
lines changed

6 files changed

+55
-23
lines changed

packages/contracts-bedrock/scripts/deploy/DeployOwnership.s.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Enum as SafeOps } from "safe-contracts/common/Enum.sol";
1111

1212
import { DeployUtils } from "scripts/libraries/DeployUtils.sol";
1313

14+
import { SafeExtensions } from "src/safe/SafeExtensions.sol";
1415
import { LivenessModule2 } from "src/safe/LivenessModule2.sol";
1516
import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol";
1617

@@ -242,7 +243,7 @@ contract DeployOwnership is Deploy {
242243
/// Note this function does not have the broadcast modifier.
243244
function deployLivenessModule() public returns (address addr_) {
244245
// Deploy the singleton LivenessModule2 (no parameters needed)
245-
addr_ = address(new LivenessModule2());
246+
addr_ = address(new SafeExtensions());
246247

247248
artifacts.save("LivenessModule2", address(addr_));
248249
console.log("New LivenessModule2 deployed at %s", address(addr_));
@@ -324,7 +325,7 @@ contract DeployOwnership is Deploy {
324325

325326
// Verify the module was configured correctly
326327
(uint256 configuredPeriod, address configuredFallback) =
327-
LivenessModule2(livenessModule).safeConfigs(address(safe));
328+
SafeExtensions(livenessModule).safeConfigs(address(safe));
328329
require(
329330
configuredPeriod == exampleCouncilConfig.livenessModuleConfig.livenessInterval,
330331
"DeployOwnership: configured liveness interval must match expected value"

packages/contracts-bedrock/src/safe/LivenessModule2.sol

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import { ISemver } from "interfaces/universal/ISemver.sol";
1414
/// when the Safe becomes unresponsive. The fallback owner can initiate a challenge,
1515
/// and if the Safe doesn't respond within the challenge period, ownership transfers
1616
/// to the fallback owner.
17-
/// @dev This is a singleton contract. To use it:
17+
/// @dev This is an abstract contract. Concrete implementations must provide version info.
18+
/// To use it:
1819
/// 1. The Safe must first enable this module using ModuleManager.enableModule()
1920
/// 2. The Safe must then configure the module by calling configure() with params
20-
contract LivenessModule2 is ISemver {
21+
abstract contract LivenessModule2 is ISemver {
2122
/// @notice Configuration for a Safe's liveness module
2223
struct ModuleConfig {
2324
uint256 livenessResponsePeriod;
@@ -81,9 +82,6 @@ contract LivenessModule2 is ISemver {
8182
/// @notice Emitted when ownership is transferred to the fallback owner
8283
event ChallengeSucceeded(address indexed safe, address fallbackOwner);
8384

84-
/// @notice Semantic version.
85-
/// @custom:semver 2.0.0
86-
string public constant version = "2.0.0";
8785

8886
/// @notice Returns challenge_start_time + liveness_response_period if challenge exists, or
8987
/// 0 if not
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
// Safe Extensions
5+
import { LivenessModule2 } from "./LivenessModule2.sol";
6+
import { TimelockGuard } from "./TimelockGuard.sol";
7+
8+
// Interfaces
9+
import { ISemver } from "interfaces/universal/ISemver.sol";
10+
11+
/// @title SafeExtensions
12+
/// @notice Combined Safe extensions providing both liveness module and timelock guard functionality
13+
/// @dev This contract can be enabled simultaneously as both a module and a guard on a Safe:
14+
/// - As a module: provides liveness challenge functionality to prevent multisig deadlock
15+
/// - As a guard: provides timelock functionality for transaction delays and cancellation
16+
contract SafeExtensions is LivenessModule2, TimelockGuard {
17+
/// @notice Semantic version.
18+
/// @custom:semver 1.0.0
19+
string public constant version = "1.0.0";
20+
}

packages/contracts-bedrock/src/safe/TimelockGuard.sol

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ import { ISemver } from "interfaces/universal/ISemver.sol";
1111

1212
/// @title TimelockGuard
1313
/// @notice This guard provides timelock functionality for Safe transactions
14-
/// @dev This is a singleton contract. To use it:
14+
/// @dev This is an abstract contract. Concrete implementations must provide version info.
15+
/// To use it:
1516
/// 1. The Safe must first enable this guard using GuardManager.setGuard()
1617
/// 2. The Safe must then configure the guard by calling configureTimelockGuard()
17-
contract TimelockGuard is IGuard, ISemver {
18+
abstract contract TimelockGuard is IGuard, ISemver {
1819
/// @notice Configuration for a Safe's timelock guard
1920
struct GuardConfig {
2021
uint256 timelockDelay;
2122
}
2223

2324
/// @notice Mapping from Safe address to its guard configuration
24-
mapping(address => GuardConfig) public safeConfigs;
25+
mapping(address => GuardConfig) public guardConfigs;
2526

2627
/// @notice Mapping from Safe address to its current cancellation threshold
2728
mapping(address => uint256) public safeCancellationThreshold;
@@ -44,17 +45,13 @@ contract TimelockGuard is IGuard, ISemver {
4445
/// @notice Emitted when a Safe clears the guard configuration
4546
event GuardCleared(address indexed safe);
4647

47-
/// @notice Semantic version.
48-
/// @custom:semver 1.0.0
49-
string public constant version = "1.0.0";
5048

5149
/// @notice Returns the timelock delay for a given Safe
5250
/// @dev MUST never revert
5351
/// @param _safe The Safe address to query
5452
/// @return The timelock delay in seconds
5553
function viewTimelockGuardConfiguration(address _safe) public view returns (uint256) {
56-
// Q: What should this return if the guard is not enabled?
57-
return safeConfigs[_safe].timelockDelay;
54+
return guardConfigs[_safe].timelockDelay;
5855
}
5956

6057
/// @notice Configure the contract as a timelock guard by setting the timelock delay
@@ -77,7 +74,7 @@ contract TimelockGuard is IGuard, ISemver {
7774
}
7875

7976
// Store the configuration for this safe
80-
safeConfigs[msg.sender].timelockDelay = _timelockDelay;
77+
guardConfigs[msg.sender].timelockDelay = _timelockDelay;
8178

8279
// Initialize cancellation threshold to 1
8380
safeCancellationThreshold[msg.sender] = 1;
@@ -91,7 +88,7 @@ contract TimelockGuard is IGuard, ISemver {
9188
/// @dev MUST emit a GuardCleared event
9289
function clearTimelockGuard() external {
9390
// Check if the calling safe has configuration set
94-
if (safeConfigs[msg.sender].timelockDelay == 0) {
91+
if (guardConfigs[msg.sender].timelockDelay == 0) {
9592
revert TimelockGuard_GuardNotConfigured();
9693
}
9794

@@ -101,7 +98,7 @@ contract TimelockGuard is IGuard, ISemver {
10198
}
10299

103100
// Erase the configuration data for this safe
104-
delete safeConfigs[msg.sender];
101+
delete guardConfigs[msg.sender];
105102
delete safeCancellationThreshold[msg.sender];
106103

107104
emit GuardCleared(msg.sender);
@@ -119,7 +116,7 @@ contract TimelockGuard is IGuard, ISemver {
119116
}
120117

121118
// Return 0 if not configured
122-
if (safeConfigs[_safe].timelockDelay == 0) {
119+
if (guardConfigs[_safe].timelockDelay == 0) {
123120
return 0;
124121
}
125122

packages/contracts-bedrock/test/safe/LivenessModule2.t.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import "test/safe-tools/SafeTestTools.sol";
77

88
import { LivenessModule2 } from "src/safe/LivenessModule2.sol";
99

10+
/// @title ConcreteLivenessModule2
11+
/// @notice Concrete implementation of LivenessModule2 for testing
12+
contract ConcreteLivenessModule2 is LivenessModule2 {
13+
/// @notice Semantic version.
14+
/// @custom:semver 2.0.0-test
15+
string public constant version = "2.0.0-test";
16+
}
17+
1018
/// @title LivenessModule2_TestInit
1119
/// @notice Reusable test initialization for `LivenessModule2` tests.
1220
contract LivenessModule2_TestInit is Test, SafeTestTools {
@@ -24,7 +32,7 @@ contract LivenessModule2_TestInit is Test, SafeTestTools {
2432
uint256 constant NUM_OWNERS = 5;
2533
uint256 constant THRESHOLD = 3;
2634

27-
LivenessModule2 livenessModule2;
35+
ConcreteLivenessModule2 livenessModule2;
2836
SafeInstance safeInstance;
2937
address fallbackOwner;
3038
address[] owners;
@@ -34,7 +42,7 @@ contract LivenessModule2_TestInit is Test, SafeTestTools {
3442
vm.warp(INIT_TIME);
3543

3644
// Deploy the singleton LivenessModule2
37-
livenessModule2 = new LivenessModule2();
45+
livenessModule2 = new ConcreteLivenessModule2();
3846

3947
// Create Safe owners
4048
(address[] memory _owners, uint256[] memory _keys) = SafeTestLib.makeAddrsAndKeys("owners", NUM_OWNERS);

packages/contracts-bedrock/test/safe/TimelockGuard.t.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import "test/safe-tools/SafeTestTools.sol";
99

1010
import { TimelockGuard } from "src/safe/TimelockGuard.sol";
1111

12+
/// @title ConcreteTimelockGuard
13+
/// @notice Concrete implementation of TimelockGuard for testing
14+
contract ConcreteTimelockGuard is TimelockGuard {
15+
/// @notice Semantic version.
16+
/// @custom:semver 1.0.0-test
17+
string public constant version = "1.0.0-test";
18+
}
19+
1220
/// @title TimelockGuard_TestInit
1321
/// @notice Reusable test initialization for `TimelockGuard` tests.
1422
contract TimelockGuard_TestInit is Test, SafeTestTools {
@@ -24,7 +32,7 @@ contract TimelockGuard_TestInit is Test, SafeTestTools {
2432
uint256 constant THRESHOLD = 3;
2533
uint256 constant ONE_YEAR = 365 days;
2634

27-
TimelockGuard timelockGuard;
35+
ConcreteTimelockGuard timelockGuard;
2836
SafeInstance safeInstance;
2937
SafeInstance safeInstance2;
3038
address[] owners;
@@ -34,7 +42,7 @@ contract TimelockGuard_TestInit is Test, SafeTestTools {
3442
vm.warp(INIT_TIME);
3543

3644
// Deploy the singleton TimelockGuard
37-
timelockGuard = new TimelockGuard();
45+
timelockGuard = new ConcreteTimelockGuard();
3846

3947
// Create Safe owners
4048
(address[] memory _owners, uint256[] memory _keys) = SafeTestLib.makeAddrsAndKeys("owners", NUM_OWNERS);

0 commit comments

Comments
 (0)