Skip to content

Commit 4e57f1e

Browse files
maureliangraphite-app[bot]claudeJosepBove
authored
Improve SaferSafes test suite. (#17949)
* Add SaferSafes as child of the module and guard * Add ISaferSafes * Test comment and assertion fixes * Improve comments * Make LivenessModule2 and TimelockGuard abstract Move semver to SaferSafes semver lock * fix test contract name * Move semver to SaferSafes * Disable the guard and module upon ownership transfer * Add _disableThisGuard function * Update tests * Add config resets * fmt * fix test_changeOwnershipToFallback_canRechallenge_succeeds * Simplify by clearing config directly * Put _disableThisGuard into child contract * Add timelockDelay reset on _disableThisGuard * semver-lock * Move _disableThisGuard logic into TimelockGuard * clear livenessSafeConfig at tend of _disableThisModule * Clarify use of SENTINEL_OWNER * Fix the ordering of the disableGuard and disableModule calls * semver-lock * remove unused imports * rename _disableThisGuard to _disableGuard * bump semver * Add test to remove unrelated guard * Add SENTINEL_MODULE constant * Clean up using ternary if * Reset cancellationThreshold to 0 on changeOwnership * Fix moduleFound if/else handling * Clear pending transactions * Pre-pr fixes * Add test contract to test name lint exclusions * fix name of test contract * Move _disableGuard impl into TimelockGuard * Add missing natspec * Add gas limit testing on changeOwnershipToFallback * Remove interfaces for abstract contracts * Move state changes out into internal _clearLivenessModule * Improve names on the internal _disableX methods * Add clearTimelockGuard function * Add _disableGuard helper to TLG tests * Limit number of transactions cancelled to 100 * Revert "Remove interfaces for abstract contracts" This reverts commit bd03288. * Move livenessModule2 address into TestUtils Reduces diff a bit * Reduce diff somewhat * Remove unused arg * Update packages/contracts-bedrock/src/safe/TimelockGuard.sol Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * Fix iface * update abi for iface fix * Do not clear or disable the module during ownership transfer * Fix inaccurate comment on _disableAndClearGuard * Further improve comment * remove unused import * fix test name * Do not clear guard during changeOwnershipToFallback * Remove unused SENTINEL_MODULE var * Remove dangling comment * Revert "Remove dangling comment" This reverts commit d266d12. * Fix whitespace * remove unnecessary internal _clearTimelockGuard function It's no longer reused in the change ownership call. * Address feedback * Add missing assertion * Move guard slot into constants * semver-lock * Remove LivenessModule from semver-lock * fix: fmt, semver-lock, unused imports * Remove unused variable * fix semver lock by resetting old LivenessModule * fix unused import * Require that msgSender be an owner of the Safe * fix compiler error * Fix placement of _msgSender check * semver-lock * Add TimelockGuard_NotOwner test * Bump semver * Add test comment, make into fuzz test * Improvements to SaferSafes styling (#17903) * Add public getter livenessSafeConfiguration to return a struct rather than tuple * Use Safe as input type to mappings and functions on LivenessModule2 * Add dividers based on function type * fmt * snapshots * Remove conditional return of 0 in the cancellationThreshold if the guard is not enabled * rename timelockConfiguration func to timelockDelay * semver-lock * Add missing natspec on tests and convert to fuzzing where possible * fix import and abi snapshot * fix: off by one error in challenge period test * fix test name * refactor(test): split clearLivenessModule tests into separate contract - Create LivenessModule2_ClearLivenessModule_Test contract - Rename test_clear_* to test_clearLivenessModule_* for consistency - Update contract title to match function under test * test(LivenessModule2): add challenge cancellation to clearLivenessModule test Enhance test_clearLivenessModule_succeeds to verify that clearing also properly cancels any active challenge, covering the _cancelChallenge code path. * test(LivenessModule2): add guard removal check to ownership transfer test Integrate guard removal verification into existing fuzz test rather than creating a separate test. Verifies that any guard set on the Safe is properly removed during the ownership transfer process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Remove pointless check Slop that got through * Convert to fuzz tests * test(TimelockGuard): enhance test coverage and convert to fuzz tests - Convert configureTimelockGuard tests to fuzz tests for broader coverage - Add comprehensive checkAfterExecution tests for all code paths - Remove redundant assertions and comments - Add test to verify transaction cannot be re-executed after success - Improve test documentation and structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix test name to match function name * feat: two extra tests to fuzz * fix: fmt --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]> Co-authored-by: JosepBove <[email protected]>
1 parent b940308 commit 4e57f1e

File tree

3 files changed

+121
-28
lines changed

3 files changed

+121
-28
lines changed

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Constants } from "src/libraries/Constants.sol";
1010
import { LivenessModule2 } from "src/safe/LivenessModule2.sol";
1111
import { SaferSafes } from "src/safe/SaferSafes.sol";
1212
import { ModuleManager } from "safe-contracts/base/ModuleManager.sol";
13+
import { GuardManager } from "safe-contracts/base/GuardManager.sol";
1314

1415
/// @title LivenessModule2_TestUtils
1516
/// @notice Reusable helper methods for LivenessModule2 tests.
@@ -108,8 +109,8 @@ contract LivenessModule2_TestInit is LivenessModule2_TestUtils {
108109
}
109110
}
110111

111-
/// @title LivenessModule2_Configure_Test
112-
/// @notice Tests configuring and clearing the module
112+
/// @title LivenessModule2_ConfigureLivenessModule_Test
113+
/// @notice Tests configuring the module
113114
contract LivenessModule2_ConfigureLivenessModule_Test is LivenessModule2_TestInit {
114115
function test_configureLivenessModule_succeeds() external {
115116
vm.expectEmit(true, true, true, true);
@@ -219,10 +220,19 @@ contract LivenessModule2_ConfigureLivenessModule_Test is LivenessModule2_TestIni
219220
challengeEndTime = livenessModule2.getChallengePeriodEnd(safeInstance.safe);
220221
assertEq(challengeEndTime, 0);
221222
}
223+
}
222224

223-
function test_clear_succeeds() external {
225+
/// @title LivenessModule2_ClearLivenessModule_Test
226+
/// @notice Tests clearing the module configuration
227+
contract LivenessModule2_ClearLivenessModule_Test is LivenessModule2_TestInit {
228+
function test_clearLivenessModule_succeeds() external {
224229
_enableModule(safeInstance, CHALLENGE_PERIOD, fallbackOwner);
225230

231+
// Start a challenge to test that clearing also cancels it
232+
vm.prank(fallbackOwner);
233+
livenessModule2.challenge(safeInstance.safe);
234+
assertGt(livenessModule2.challengeStartTime(safeInstance.safe), 0);
235+
226236
// First disable the module at the Safe level
227237
SafeTestLib.execTransaction(
228238
safeInstance,
@@ -232,6 +242,9 @@ contract LivenessModule2_ConfigureLivenessModule_Test is LivenessModule2_TestIni
232242
Enum.Operation.Call
233243
);
234244

245+
// Clear should emit ChallengeCancelled then ModuleCleared
246+
vm.expectEmit(true, true, true, true);
247+
emit ChallengeCancelled(address(safeInstance.safe));
235248
vm.expectEmit(true, true, true, true);
236249
emit ModuleCleared(address(safeInstance.safe));
237250

@@ -247,15 +260,16 @@ contract LivenessModule2_ConfigureLivenessModule_Test is LivenessModule2_TestIni
247260
LivenessModule2.ModuleConfig memory clearedConfig = livenessModule2.livenessSafeConfiguration(safeInstance.safe);
248261
assertEq(clearedConfig.livenessResponsePeriod, 0);
249262
assertEq(clearedConfig.fallbackOwner, address(0));
263+
assertEq(livenessModule2.challengeStartTime(safeInstance.safe), 0);
250264
}
251265

252-
function test_clear_notEnabled_reverts() external {
266+
function test_clearLivenessModule_notConfigured_reverts() external {
253267
vm.expectRevert(LivenessModule2.LivenessModule2_ModuleNotConfigured.selector);
254268
vm.prank(address(safeInstance.safe));
255269
livenessModule2.clearLivenessModule();
256270
}
257271

258-
function test_clear_moduleStillEnabled_reverts() external {
272+
function test_clearLivenessModule_moduleStillEnabled_reverts() external {
259273
_enableModule(safeInstance, CHALLENGE_PERIOD, fallbackOwner);
260274

261275
// Try to clear while module is still enabled (should revert)
@@ -450,6 +464,17 @@ contract LivenessModule2_ChangeOwnershipToFallback_Test is LivenessModule2_TestI
450464
// Bound time to reasonable values (1 second to 365 days after expiry)
451465
timeAfterExpiry = bound(timeAfterExpiry, 1, 365 days);
452466

467+
// Set a guard to verify it gets removed
468+
address mockGuard = makeAddr("mockGuard");
469+
SafeTestLib.execTransaction(
470+
safeInstance,
471+
address(safeInstance.safe),
472+
0,
473+
abi.encodeCall(GuardManager.setGuard, (mockGuard)),
474+
Enum.Operation.Call
475+
);
476+
assertEq(_getGuard(safeInstance), mockGuard);
477+
453478
// Start a challenge
454479
vm.prank(fallbackOwner);
455480
livenessModule2.challenge(safeInstance.safe);
@@ -473,6 +498,9 @@ contract LivenessModule2_ChangeOwnershipToFallback_Test is LivenessModule2_TestI
473498
// Verify challenge is reset
474499
uint256 challengeEndTime = livenessModule2.getChallengePeriodEnd(safeInstance.safe);
475500
assertEq(challengeEndTime, 0);
501+
502+
// Verify guard was removed
503+
assertEq(_getGuard(safeInstance), address(0));
476504
}
477505

478506
/// @notice Tests that changeOwnershipToFallback reverts if module is not configured

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity 0.8.15;
33

44
import { Enum } from "safe-contracts/common/Enum.sol";
5+
import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol";
56
import "test/safe-tools/SafeTestTools.sol";
67

78
import { SaferSafes } from "src/safe/SaferSafes.sol";

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

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -261,49 +261,46 @@ abstract contract TimelockGuard_TestInit is Test, SafeTestTools {
261261
}
262262

263263
/// @title TimelockGuard_TimelockDelay_Test
264-
/// @notice Tests for timelockConfiguration function
264+
/// @notice Tests for TimelockDelay function
265265
contract TimelockGuard_TimelockDelay_Test is TimelockGuard_TestInit {
266266
/// @notice Ensures an unconfigured Safe reports a zero timelock delay.
267-
function test_timelockConfiguration_returnsZeroForUnconfiguredSafe_succeeds() external view {
267+
function test_timelockDelay_returnsZeroForUnconfiguredSafe_succeeds() external view {
268268
uint256 delay = timelockGuard.timelockDelay(safeInstance.safe);
269269
assertEq(delay, 0);
270-
// configured is now determined by timelockDelay == 0
271-
assertEq(delay == 0, true);
272270
}
273271

274-
/// @notice Validates the configuration view reflects the stored timelock delay.
275-
function test_timelockConfiguration_returnsConfigurationForConfiguredSafe_succeeds() external {
276-
_configureGuard(safeInstance, TIMELOCK_DELAY);
277-
uint256 delay = timelockGuard.timelockDelay(safeInstance.safe);
278-
assertEq(delay, TIMELOCK_DELAY);
279-
// configured is now determined by timelockDelay != 0
280-
assertEq(delay != 0, true);
272+
/// @notice Fuzz test: Validates the configuration view reflects the stored timelock delay for any valid delay.
273+
function testFuzz_timelockDelay_returnsConfigurationForConfiguredSafe_succeeds(uint256 _delay_) external {
274+
_delay_ = bound(_delay_, 1, ONE_YEAR); // Restrict to valid range
275+
_configureGuard(safeInstance, _delay_);
276+
uint256 delay_ = timelockGuard.timelockDelay(safeInstance.safe);
277+
assertEq(delay_, _delay_);
281278
}
282279
}
283280

284281
/// @title TimelockGuard_ConfigureTimelockGuard_Test
285282
/// @notice Tests for configureTimelockGuard function
286283
contract TimelockGuard_ConfigureTimelockGuard_Test is TimelockGuard_TestInit {
287-
/// @notice Verifies the guard can be configured with a standard delay.
288-
function test_configureTimelockGuard_succeeds() external {
284+
/// @notice Verifies the guard can be configured with various valid delays.
285+
function testFuzz_configureTimelockGuard_validDelay_succeeds(uint256 _delay) external {
286+
_delay = bound(_delay, 1, ONE_YEAR);
287+
289288
vm.expectEmit(true, true, true, true);
290-
emit GuardConfigured(safe, TIMELOCK_DELAY);
289+
emit GuardConfigured(safe, _delay);
291290

292-
_configureGuard(safeInstance, TIMELOCK_DELAY);
291+
_configureGuard(safeInstance, _delay);
293292

294293
uint256 delay = timelockGuard.timelockDelay(safe);
295-
assertEq(delay, TIMELOCK_DELAY);
296-
// configured is now determined by timelockDelay != 0
297-
assertEq(delay != 0, true);
294+
assertEq(delay, _delay);
298295
}
299296

300297
/// @notice Confirms delays above the maximum revert during configuration.
301-
function test_configureTimelockGuard_revertsIfDelayTooLong_reverts() external {
302-
uint256 tooLongDelay = ONE_YEAR + 1;
298+
function testFuzz_configureTimelockGuard_delayTooLong_reverts(uint256 _delay) external {
299+
_delay = bound(_delay, ONE_YEAR + 1, type(uint256).max);
303300

304301
vm.expectRevert(TimelockGuard.TimelockGuard_InvalidTimelockDelay.selector);
305302
vm.prank(address(safeInstance.safe));
306-
timelockGuard.configureTimelockGuard(tooLongDelay);
303+
timelockGuard.configureTimelockGuard(_delay);
307304
}
308305

309306
/// @notice Checks configuration reverts when the contract is too old.
@@ -324,8 +321,6 @@ contract TimelockGuard_ConfigureTimelockGuard_Test is TimelockGuard_TestInit {
324321

325322
uint256 delay = timelockGuard.timelockDelay(safe);
326323
assertEq(delay, ONE_YEAR);
327-
// configured is now determined by timelockDelay != 0
328-
assertEq(delay != 0, true);
329324
}
330325

331326
/// @notice Demonstrates the guard can be reconfigured to a new delay.
@@ -733,6 +728,75 @@ contract TimelockGuard_CheckTransaction_Test is TimelockGuard_TestInit {
733728
}
734729
}
735730

731+
/// @title TimelockGuard_CheckAfterExecution_Test
732+
/// @notice Tests for checkAfterExecution function
733+
contract TimelockGuard_CheckAfterExecution_Test is TimelockGuard_TestInit {
734+
function setUp() public override {
735+
super.setUp();
736+
_configureGuard(safeInstance, TIMELOCK_DELAY);
737+
}
738+
739+
/// @notice Verifies successful execution updates state and resets threshold.
740+
function test_checkAfterExecution_successfulExecution_succeeds() external {
741+
TransactionBuilder.Transaction memory dummyTx = _createDummyTransaction(safeInstance);
742+
dummyTx.scheduleTransaction(timelockGuard);
743+
744+
uint256 expectedExecutionTime = block.timestamp + TIMELOCK_DELAY;
745+
vm.warp(expectedExecutionTime);
746+
747+
// Verify initial cancellation threshold
748+
uint256 initialThreshold = timelockGuard.cancellationThreshold(safeInstance.safe);
749+
assertEq(initialThreshold, 1);
750+
751+
// Call checkAfterExecution with successful execution
752+
vm.expectEmit(true, true, true, true);
753+
emit TransactionExecuted(safeInstance.safe, dummyTx.hash);
754+
vm.prank(address(safeInstance.safe));
755+
timelockGuard.checkAfterExecution(dummyTx.hash, true);
756+
757+
// Verify transaction state changed to Executed
758+
TimelockGuard.ScheduledTransaction memory scheduledTx =
759+
timelockGuard.scheduledTransaction(safeInstance.safe, dummyTx.hash);
760+
assertEq(uint256(scheduledTx.state), uint256(TimelockGuard.TransactionState.Executed));
761+
762+
// Verify transaction removed from pending list
763+
TimelockGuard.ScheduledTransaction[] memory pending = timelockGuard.pendingTransactions(safeInstance.safe);
764+
assertEq(pending.length, 0);
765+
766+
// Verify cancellation threshold was reset to 1
767+
assertEq(timelockGuard.cancellationThreshold(safeInstance.safe), 1);
768+
769+
// Verify transaction cannot be executed again
770+
vm.expectRevert(TimelockGuard.TimelockGuard_TransactionAlreadyExecuted.selector);
771+
dummyTx.executeTransaction(safeInstance.owners[0]);
772+
}
773+
774+
/// @notice Verifies transaction state remains unchanged on execution failure.
775+
function test_checkAfterExecution_failedExecution_succeeds() external {
776+
TransactionBuilder.Transaction memory dummyTx = _createDummyTransaction(safeInstance);
777+
dummyTx.scheduleTransaction(timelockGuard);
778+
779+
uint256 expectedExecutionTime = block.timestamp + TIMELOCK_DELAY;
780+
vm.warp(expectedExecutionTime);
781+
782+
// Call checkAfterExecution with failed execution
783+
vm.prank(address(safeInstance.safe));
784+
timelockGuard.checkAfterExecution(dummyTx.hash, false);
785+
786+
// Verify transaction state remains unchanged
787+
TimelockGuard.ScheduledTransaction memory scheduledTx =
788+
timelockGuard.scheduledTransaction(safeInstance.safe, dummyTx.hash);
789+
assertEq(uint256(scheduledTx.state), uint256(TimelockGuard.TransactionState.Pending));
790+
assertEq(uint256(scheduledTx.executionTime), expectedExecutionTime);
791+
}
792+
793+
/// @notice Fuzz test: Verifies unconfigured guard allows checkAfterExecution for any _hash.
794+
function testFuzz_checkAfterExecution_unconfiguredGuard_succeeds(bytes32 _hash) external {
795+
vm.prank(address(unguardedSafe.safe));
796+
timelockGuard.checkAfterExecution(_hash, true);
797+
}
798+
}
799+
736800
/// @title TimelockGuard_MaxCancellationThreshold_Test
737801
/// @notice Tests for the maxCancellationThreshold function in TimelockGuard
738802
contract TimelockGuard_MaxCancellationThreshold_Test is TimelockGuard_TestInit {

0 commit comments

Comments
 (0)