diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 64aecdda..b82f8272 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -14,11 +14,8 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { error InvalidGovernance(address governance); error NotGovernance(address account, address governance); - error SchedulingDisabled(); - error UnscheduledExecutionForbidden(); event GovernanceSet(address governance); - event ProposalLaunched(address indexed proposer, address indexed executor, uint256 indexed proposalId); address internal _governance; @@ -30,7 +27,6 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { _checkGovernance(msg.sender); newProposalId = _proposals.submit(executor, calls); - emit ProposalLaunched(msg.sender, executor, newProposalId); } function schedule(uint256 proposalId) external returns (uint256 submittedAt) { diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 93da9847..c1034658 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -26,6 +26,10 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { TIMELOCK.schedule(proposalId); } + function executeProposal(uint256 proposalId) external { + TIMELOCK.execute(proposalId); + } + function canSchedule(uint256 proposalId) external view returns (bool) { return TIMELOCK.canSchedule(proposalId); } diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 758525e1..bbbee390 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -15,13 +15,11 @@ struct EmergencyState { library EmergencyProtection { error NotEmergencyActivator(address account); error NotEmergencyEnactor(address account); - error EmergencyPeriodFinished(); - error EmergencyCommitteeExpired(); + error EmergencyCommitteeExpired(uint256 timestamp, uint256 protectedTill); error InvalidEmergencyModeActiveValue(bool actual, bool expected); - event EmergencyModeActivated(); - event EmergencyModeDeactivated(); - event EmergencyGovernanceReset(); + event EmergencyModeActivated(uint256 timestamp); + event EmergencyModeDeactivated(uint256 timestamp); event EmergencyActivationCommitteeSet(address indexed activationCommittee); event EmergencyExecutionCommitteeSet(address indexed executionCommittee); event EmergencyModeDurationSet(uint256 emergencyModeDuration); @@ -74,10 +72,10 @@ library EmergencyProtection { function activate(State storage self) internal { if (block.timestamp > self.protectedTill) { - revert EmergencyCommitteeExpired(); + revert EmergencyCommitteeExpired(block.timestamp, self.protectedTill); } self.emergencyModeEndsAfter = SafeCast.toUint40(block.timestamp + self.emergencyModeDuration); - emit EmergencyModeActivated(); + emit EmergencyModeActivated(block.timestamp); } function deactivate(State storage self) internal { @@ -86,7 +84,7 @@ library EmergencyProtection { self.protectedTill = 0; self.emergencyModeDuration = 0; self.emergencyModeEndsAfter = 0; - emit EmergencyModeDeactivated(); + emit EmergencyModeDeactivated(block.timestamp); } function getEmergencyState(State storage self) internal view returns (EmergencyState memory res) { diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index 3f5b742a..e404a93f 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -10,7 +10,7 @@ enum Status { Submitted, Scheduled, Executed, - Canceled + Cancelled } struct Proposal { @@ -34,12 +34,12 @@ library Proposals { struct State { // any proposals with ids less or equal to the given one cannot be executed - uint256 lastCanceledProposalId; + uint256 lastCancelledProposalId; ProposalPacked[] proposals; } error EmptyCalls(); - error ProposalCanceled(uint256 proposalId); + error ProposalCancelled(uint256 proposalId); error ProposalNotFound(uint256 proposalId); error ProposalNotScheduled(uint256 proposalId); error ProposalNotSubmitted(uint256 proposalId); @@ -49,7 +49,7 @@ library Proposals { event ProposalScheduled(uint256 indexed id); event ProposalSubmitted(uint256 indexed id, address indexed executor, ExecutorCall[] calls); event ProposalExecuted(uint256 indexed id, bytes[] callResults); - event ProposalsCanceledTill(uint256 proposalId); + event ProposalsCancelledTill(uint256 proposalId); // The id of the first proposal uint256 private constant PROPOSAL_ID_OFFSET = 1; @@ -57,7 +57,7 @@ library Proposals { function submit( State storage self, address executor, - ExecutorCall[] calldata calls + ExecutorCall[] memory calls ) internal returns (uint256 newProposalId) { if (calls.length == 0) { revert EmptyCalls(); @@ -67,9 +67,8 @@ library Proposals { self.proposals.push(); ProposalPacked storage newProposal = self.proposals[newProposalIndex]; - newProposal.executor = executor; - newProposal.executedAt = 0; + newProposal.executor = executor; newProposal.submittedAt = TimeUtils.timestamp(); // copying of arrays of custom types from calldata to storage has not been supported by the @@ -105,8 +104,8 @@ library Proposals { function cancelAll(State storage self) internal { uint256 lastProposalId = self.proposals.length; - self.lastCanceledProposalId = lastProposalId; - emit ProposalsCanceledTill(lastProposalId); + self.lastCancelledProposalId = lastProposalId; + emit ProposalsCancelledTill(lastProposalId); } function get(State storage self, uint256 proposalId) internal view returns (Proposal memory proposal) { @@ -117,6 +116,7 @@ library Proposals { proposal.status = _getProposalStatus(self, proposalId); proposal.executor = packed.executor; proposal.submittedAt = packed.submittedAt; + proposal.scheduledAt = packed.scheduledAt; proposal.executedAt = packed.executedAt; proposal.calls = packed.calls; } @@ -143,7 +143,7 @@ library Proposals { && block.timestamp >= _packed(self, proposalId).submittedAt + afterSubmitDelay; } - function _executeProposal(State storage self, uint256 proposalId) private returns (bytes[] memory results) { + function _executeProposal(State storage self, uint256 proposalId) private { ProposalPacked storage packed = _packed(self, proposalId); packed.executedAt = TimeUtils.timestamp(); @@ -153,7 +153,7 @@ library Proposals { assert(callsCount > 0); address executor = packed.executor; - results = new bytes[](callsCount); + bytes[] memory results = new bytes[](callsCount); for (uint256 i = 0; i < callsCount; ++i) { results[i] = IExecutor(payable(executor)).execute(calls[i].target, calls[i].value, calls[i].payload); } @@ -204,13 +204,13 @@ library Proposals { } } - function _getProposalStatus(State storage self, uint256 proposalId) private view returns (Status) { + function _getProposalStatus(State storage self, uint256 proposalId) private view returns (Status status) { if (proposalId < PROPOSAL_ID_OFFSET || proposalId > self.proposals.length) return Status.NotExist; ProposalPacked storage packed = _packed(self, proposalId); if (packed.executedAt != 0) return Status.Executed; - if (proposalId <= self.lastCanceledProposalId) return Status.Canceled; + if (proposalId <= self.lastCancelledProposalId) return Status.Cancelled; if (packed.scheduledAt != 0) return Status.Scheduled; if (packed.submittedAt != 0) return Status.Submitted; assert(false); diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol new file mode 100644 index 00000000..7216c248 --- /dev/null +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -0,0 +1,856 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {IConfiguration, Configuration} from "contracts/Configuration.sol"; +import {ConfigurationProvider} from "contracts/ConfigurationProvider.sol"; +import {Executor} from "contracts/Executor.sol"; +import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; +import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/utils.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; + +contract EmergencyProtectedTimelockUnitTests is UnitTest { + EmergencyProtectedTimelock private _timelock; + Configuration private _config; + TargetMock private _targetMock; + Executor private _executor; + + address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); + address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); + uint256 private _emergencyModeDuration = 180 days; + uint256 private _emergencyProtectionDuration = 90 days; + + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); + address private _adminExecutor; + + function setUp() external { + _executor = new Executor(address(this)); + _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); + _timelock = new EmergencyProtectedTimelock(address(_config)); + _targetMock = new TargetMock(); + + _executor.transferOwnership(address(_timelock)); + _adminExecutor = address(_executor); + + vm.startPrank(_adminExecutor); + _timelock.setGovernance(_dualGovernance); + _timelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + vm.stopPrank(); + } + + // EmergencyProtectedTimelock.submit() + + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.submit(_adminExecutor, new ExecutorCall[](0)); + assertEq(_timelock.getProposalsCount(), 0); + } + + function test_governance_can_submit_proposal() external { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + + assertEq(_timelock.getProposalsCount(), 1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + } + + // EmergencyProtectedTimelock.schedule() + + function test_governance_can_schedule_proposal() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Scheduled); + } + + function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + _submitProposal(); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.schedule(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + } + + // EmergencyProtectedTimelock.execute() + + function testFuzz_anyone_can_execute_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + _submitProposal(); + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + vm.prank(stranger); + _timelock.execute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Executed); + } + + function test_cannot_execute_proposal_if_emergency_mode_active() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + _scheduleProposal(1); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) + ); + _timelock.execute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Scheduled); + } + + // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() + + function test_governance_can_cancel_all_non_executed_proposals() external { + _submitProposal(); + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 2); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + Proposal memory proposal1 = _timelock.getProposal(1); + Proposal memory proposal2 = _timelock.getProposal(2); + + assert(proposal1.status == Status.Scheduled); + assert(proposal2.status == Status.Submitted); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + proposal1 = _timelock.getProposal(1); + proposal2 = _timelock.getProposal(2); + + assertEq(_timelock.getProposalsCount(), 2); + assert(proposal1.status == Status.Cancelled); + assert(proposal2.status == Status.Cancelled); + } + + function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.cancelAllNonExecutedProposals(); + } + + // EmergencyProtectedTimelock.transferExecutorOwnership() + + function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { + vm.assume(newOwner != _adminExecutor); + vm.assume(newOwner != address(0)); + + Executor executor = new Executor(address(_timelock)); + + assertEq(executor.owner(), address(_timelock)); + + vm.prank(_adminExecutor); + + vm.expectEmit(address(executor)); + emit Ownable.OwnershipTransferred(address(_timelock), newOwner); + + _timelock.transferExecutorOwnership(address(executor), newOwner); + + assertEq(executor.owner(), newOwner); + } + + function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); + } + + // EmergencyProtectedTimelock.setGovernance() + + function testFuzz_admin_executor_can_set_governance(address newGovernance) external { + vm.assume(newGovernance != _dualGovernance); + vm.assume(newGovernance != address(0)); + + vm.expectEmit(address(_timelock)); + emit EmergencyProtectedTimelock.GovernanceSet(newGovernance); + + vm.recordLogs(); + vm.prank(_adminExecutor); + _timelock.setGovernance(newGovernance); + + assertEq(_timelock.getGovernance(), newGovernance); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + } + + function test_cannot_set_governance_to_zero() external { + vm.prank(_adminExecutor); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, address(0))); + _timelock.setGovernance(address(0)); + } + + function test_cannot_set_governance_to_the_same_address() external { + address currentGovernance = _timelock.getGovernance(); + vm.prank(_adminExecutor); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, _dualGovernance)); + _timelock.setGovernance(currentGovernance); + + assertEq(_timelock.getGovernance(), currentGovernance); + } + + function testFuzz_stranger_cannot_set_governance(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.setGovernance(makeAddr("newGovernance")); + } + + // EmergencyProtectedTimelock.activateEmergencyMode() + + function test_emergency_activator_can_activate_emergency_mode() external { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + } + + function testFuzz_stranger_cannot_activate_emergency_mode(address stranger) external { + vm.assume(stranger != _emergencyActivator); + vm.assume(stranger != address(0)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, stranger)); + _timelock.activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), false); + } + + function test_cannot_activate_emergency_mode_if_already_active() external { + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(_emergencyActivator); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) + ); + _timelock.activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + } + + // EmergencyProtectedTimelock.emergencyExecute() + + function test_emergency_executior_can_execute_proposal() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(_emergencyEnactor); + _timelock.emergencyExecute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Executed); + } + + function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { + vm.startPrank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + _timelock.schedule(1); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + vm.stopPrank(); + + EmergencyState memory state = _timelock.getEmergencyState(); + assertEq(state.isEmergencyModeActivated, false); + + vm.prank(_emergencyActivator); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.emergencyExecute(1); + } + + function testFuzz_stranger_cannot_emergency_execute_proposal(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); + + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + _timelock.emergencyExecute(1); + } + + // EmergencyProtectedTimelock.deactivateEmergencyMode() + + function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { + _activateEmergencyMode(); + + vm.prank(_adminExecutor); + _timelock.deactivateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), false); + } + + function test_after_deactivation_all_proposals_are_cancelled() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + + _activateEmergencyMode(); + + _deactivateEmergencyMode(); + + proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Cancelled); + } + + function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { + vm.assume(stranger != _adminExecutor); + + _activateEmergencyMode(); + + EmergencyState memory state = _timelock.getEmergencyState(); + assertEq(_isEmergencyStateActivated(), true); + + _wait(state.emergencyModeDuration + 1); + + vm.prank(stranger); + _timelock.deactivateEmergencyMode(); + + state = _timelock.getEmergencyState(); + assertEq(_isEmergencyStateActivated(), false); + } + + function testFuzz_cannot_deactivate_emergency_mode_if_not_activated(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.deactivateEmergencyMode(); + + vm.prank(_adminExecutor); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.deactivateEmergencyMode(); + } + + function testFuzz_stranger_cannot_deactivate_emergency_mode_if_not_passed(address stranger) external { + vm.assume(stranger != _adminExecutor); + + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.deactivateEmergencyMode(); + } + + // EmergencyProtectedTimelock.emergencyReset() + + function test_execution_committee_can_emergency_reset() external { + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + assertEq(_timelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + EmergencyState memory newState = _timelock.getEmergencyState(); + + assertEq(_isEmergencyStateActivated(), false); + assertEq(_timelock.getGovernance(), _emergencyGovernance); + assertEq(_timelock.isEmergencyProtectionEnabled(), false); + + assertEq(newState.activationCommittee, address(0)); + assertEq(newState.executionCommittee, address(0)); + assertEq(newState.protectedTill, 0); + assertEq(newState.emergencyModeDuration, 0); + assertEq(newState.emergencyModeEndsAfter, 0); + } + + function test_after_emergency_reset_all_proposals_are_cancelled() external { + _submitProposal(); + _activateEmergencyMode(); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Cancelled); + } + + function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + _timelock.emergencyReset(); + + assertEq(_isEmergencyStateActivated(), true); + } + + function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { + assertEq(_isEmergencyStateActivated(), false); + + EmergencyState memory state = _timelock.getEmergencyState(); + + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + EmergencyState memory newState = _timelock.getEmergencyState(); + + assertEq(newState.executionCommittee, state.executionCommittee); + assertEq(newState.activationCommittee, state.activationCommittee); + assertEq(newState.protectedTill, state.protectedTill); + assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); + assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); + assertEq(newState.isEmergencyModeActivated, state.isEmergencyModeActivated); + } + + // EmergencyProtectedTimelock.setEmergencyProtection() + + function test_admin_executor_can_set_emenrgency_protection() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { + vm.assume(stranger != _adminExecutor); + vm.assume(stranger != address(0)); + + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + // EmergencyProtectedTimelock.isEmergencyProtectionEnabled() + + function test_is_emergency_protection_enabled_deactivate() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + function test_is_emergency_protection_enabled_reset() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + // EmergencyProtectedTimelock.getEmergencyState() + + function test_get_emergency_state_deactivate() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + state = _localTimelock.getEmergencyState(); + + assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, 0); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + state = _localTimelock.getEmergencyState(); + + assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, block.timestamp + _emergencyModeDuration); + + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); + + state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + } + + function test_get_emergency_state_reset() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + } + + // EmergencyProtectedTimelock.getGovernance() + + function testFuzz_get_governance(address governance) external { + vm.assume(governance != address(0)); + vm.prank(_adminExecutor); + _timelock.setGovernance(governance); + assertEq(_timelock.getGovernance(), governance); + } + + // EmergencyProtectedTimelock.getProposal() + + function test_get_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); + + vm.startPrank(_dualGovernance); + ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(address(_targetMock)); + _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, executorCalls); + + Proposal memory submittedProposal = _timelock.getProposal(1); + + uint256 submitTimestamp = block.timestamp; + + assertEq(submittedProposal.id, 1); + assertEq(submittedProposal.executor, _adminExecutor); + assertEq(submittedProposal.submittedAt, submitTimestamp); + assertEq(submittedProposal.scheduledAt, 0); + assertEq(submittedProposal.executedAt, 0); + // assertEq doesn't support comparing enumerables so far + assert(submittedProposal.status == Status.Submitted); + assertEq(submittedProposal.calls.length, 1); + assertEq(submittedProposal.calls[0].value, executorCalls[0].value); + assertEq(submittedProposal.calls[0].target, executorCalls[0].target); + assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _timelock.schedule(1); + uint256 scheduleTimestamp = block.timestamp; + + Proposal memory scheduledProposal = _timelock.getProposal(1); + + assertEq(scheduledProposal.id, 1); + assertEq(scheduledProposal.executor, _adminExecutor); + assertEq(scheduledProposal.submittedAt, submitTimestamp); + assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); + assertEq(scheduledProposal.executedAt, 0); + // // assertEq doesn't support comparing enumerables so far + assert(scheduledProposal.status == Status.Scheduled); + assertEq(scheduledProposal.calls.length, 1); + assertEq(scheduledProposal.calls[0].value, executorCalls[0].value); + assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); + assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + _timelock.execute(1); + + Proposal memory executedProposal = _timelock.getProposal(1); + uint256 executeTimestamp = block.timestamp; + + assertEq(executedProposal.id, 1); + assertEq(executedProposal.executor, _adminExecutor); + assertEq(executedProposal.submittedAt, submitTimestamp); + assertEq(executedProposal.scheduledAt, scheduleTimestamp); + assertEq(executedProposal.executedAt, executeTimestamp); + // assertEq doesn't support comparing enumerables so far + assert(executedProposal.status == Status.Executed); + assertEq(executedProposal.calls.length, 1); + assertEq(executedProposal.calls[0].value, executorCalls[0].value); + assertEq(executedProposal.calls[0].target, executorCalls[0].target); + assertEq(executedProposal.calls[0].payload, executorCalls[0].payload); + + _timelock.cancelAllNonExecutedProposals(); + + Proposal memory cancelledProposal = _timelock.getProposal(2); + + assertEq(cancelledProposal.id, 2); + assertEq(cancelledProposal.executor, _adminExecutor); + assertEq(cancelledProposal.submittedAt, submitTimestamp); + assertEq(cancelledProposal.scheduledAt, 0); + assertEq(cancelledProposal.executedAt, 0); + // assertEq doesn't support comparing enumerables so far + assert(cancelledProposal.status == Status.Cancelled); + assertEq(cancelledProposal.calls.length, 1); + assertEq(cancelledProposal.calls[0].value, executorCalls[0].value); + assertEq(cancelledProposal.calls[0].target, executorCalls[0].target); + assertEq(cancelledProposal.calls[0].payload, executorCalls[0].payload); + } + + function test_get_not_existing_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); + + vm.expectRevert(); + _timelock.getProposal(1); + } + + // EmergencyProtectedTimelock.getProposalsCount() + + function testFuzz_get_proposals_count(uint256 count) external { + vm.assume(count > 0); + vm.assume(count <= type(uint8).max); + assertEq(_timelock.getProposalsCount(), 0); + + for (uint256 i = 1; i <= count; i++) { + _submitProposal(); + assertEq(_timelock.getProposalsCount(), i); + } + } + + // EmergencyProtectedTimelock.canExecute() + + function test_can_execute() external { + assertEq(_timelock.canExecute(1), false); + _submitProposal(); + assertEq(_timelock.canExecute(1), false); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + assertEq(_timelock.canExecute(1), false); + + _wait(_config.AFTER_SCHEDULE_DELAY()); + + assertEq(_timelock.canExecute(1), true); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + assertEq(_timelock.canExecute(1), false); + } + + // EmergencyProtectedTimelock.canSchedule() + + function test_can_schedule() external { + assertEq(_timelock.canExecute(1), false); + _submitProposal(); + assertEq(_timelock.canSchedule(1), false); + + _wait(_config.AFTER_SUBMIT_DELAY()); + + assertEq(_timelock.canSchedule(1), true); + + _scheduleProposal(1); + + assertEq(_timelock.canSchedule(1), false); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + assertEq(_timelock.canSchedule(1), false); + } + + // Utils + + function _submitProposal() internal { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + } + + function _scheduleProposal(uint256 proposalId) internal { + vm.prank(_dualGovernance); + _timelock.schedule(proposalId); + } + + function _isEmergencyStateActivated() internal view returns (bool) { + EmergencyState memory state = _timelock.getEmergencyState(); + return state.isEmergencyModeActivated; + } + + function _activateEmergencyMode() internal { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + } + + function _deactivateEmergencyMode() internal { + vm.prank(_adminExecutor); + _timelock.deactivateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), false); + } +} diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol new file mode 100644 index 00000000..489370bf --- /dev/null +++ b/test/unit/SingleGovernance.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Test.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {SingleGovernance} from "contracts/SingleGovernance.sol"; +import {IConfiguration, Configuration} from "contracts/Configuration.sol"; +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/utils.sol"; + +import {TimelockMock} from "./mocks/TimelockMock.sol"; + +contract SingleGovernanceUnitTests is UnitTest { + TimelockMock private _timelock; + SingleGovernance private _singleGovernance; + Configuration private _config; + + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _governance = makeAddr("GOVERNANCE"); + + function setUp() external { + Executor _executor = new Executor(address(this)); + _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); + _timelock = new TimelockMock(); + _singleGovernance = new SingleGovernance(address(_config), _governance, address(_timelock)); + } + + function testFuzz_constructor(address governance, address timelock) external { + SingleGovernance instance = new SingleGovernance(address(_config), governance, timelock); + + assertEq(instance.GOVERNANCE(), governance); + assertEq(address(instance.TIMELOCK()), address(timelock)); + } + + function test_submit_proposal() external { + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 1); + } + + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.startPrank(stranger); + vm.expectRevert( + abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) + ); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 0); + } + + function test_schedule_proposal() external { + assertEq(_timelock.getScheduledProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + assertEq(_timelock.getScheduledProposals().length, 1); + } + + function test_execute_proposal() external { + assertEq(_timelock.getExecutedProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + _singleGovernance.executeProposal(1); + + assertEq(_timelock.getExecutedProposals().length, 1); + } + + function test_cancel_all_pending_proposals() external { + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + _singleGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 2); + } + + function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(stranger); + vm.expectRevert( + abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) + ); + _singleGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + } + + function test_can_schedule() external { + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertFalse(_singleGovernance.canSchedule(1)); + + _timelock.setSchedule(1); + + assertTrue(_singleGovernance.canSchedule(1)); + } +} \ No newline at end of file diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol new file mode 100644 index 00000000..442c708f --- /dev/null +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Test, Vm} from "forge-std/Test.sol"; + +import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EmergencyProtectionUnitTests is UnitTest { + using EmergencyProtection for EmergencyProtection.State; + + EmergencyProtection.State internal _emergencyProtection; + + function testFuzz_setup_emergency_protection( + address activationCommittee, + address executionCommittee, + uint256 protectedTill, + uint256 duration + ) external { + vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); + vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(activationCommittee != address(0)); + vm.assume(executionCommittee != address(0)); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + protectedTill); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(duration); + + vm.recordLogs(); + + _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 4); + + assertEq(_emergencyProtection.activationCommittee, activationCommittee); + assertEq(_emergencyProtection.executionCommittee, executionCommittee); + assertEq(_emergencyProtection.protectedTill, block.timestamp + protectedTill); + assertEq(_emergencyProtection.emergencyModeDuration, duration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_activation_committee() external { + address activationCommittee = makeAddr("activationCommittee"); + + _emergencyProtection.setup(activationCommittee, address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(300); + + vm.recordLogs(); + _emergencyProtection.setup(activationCommittee, address(0x3), 200, 300); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, activationCommittee); + assertEq(_emergencyProtection.executionCommittee, address(0x3)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 300); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_execution_committee() external { + address executionCommittee = makeAddr("executionCommittee"); + + _emergencyProtection.setup(address(0x1), executionCommittee, 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(300); + + vm.recordLogs(); + _emergencyProtection.setup(address(0x2), executionCommittee, 200, 300); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x2)); + assertEq(_emergencyProtection.executionCommittee, executionCommittee); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 300); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_protected_till() external { + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(200); + + vm.recordLogs(); + _emergencyProtection.setup(address(0x3), address(0x4), 100, 200); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x3)); + assertEq(_emergencyProtection.executionCommittee, address(0x4)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 100); + assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_emergency_mode_duration() external { + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + + vm.recordLogs(); + _emergencyProtection.setup(address(0x3), address(0x4), 200, 100); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x3)); + assertEq(_emergencyProtection.executionCommittee, address(0x4)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 100); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_activate_emergency_mode() external { + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeActivated(block.timestamp); + + vm.recordLogs(); + + _emergencyProtection.activate(); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(_emergencyProtection.emergencyModeEndsAfter, block.timestamp + 100); + } + + function test_cannot_activate_emergency_mode_if_protected_till_expired() external { + uint256 protectedTill = 100; + _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, 100); + + _wait(protectedTill + 1); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyCommitteeExpired.selector, + [block.timestamp, _emergencyProtection.protectedTill] + ) + ); + _emergencyProtection.activate(); + } + + function testFuzz_deactivate_emergency_mode( + address activationCommittee, + address executionCommittee, + uint256 protectedTill, + uint256 duration + ) external { + vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); + vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(activationCommittee != address(0)); + vm.assume(executionCommittee != address(0)); + + _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.activate(); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDeactivated(block.timestamp); + + vm.recordLogs(); + + _emergencyProtection.deactivate(); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + + assertEq(_emergencyProtection.activationCommittee, address(0)); + assertEq(_emergencyProtection.executionCommittee, address(0)); + assertEq(_emergencyProtection.protectedTill, 0); + assertEq(_emergencyProtection.emergencyModeDuration, 0); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_get_emergency_state() external { + EmergencyState memory state = _emergencyProtection.getEmergencyState(); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + + _emergencyProtection.setup(address(0x1), address(0x2), 100, 200); + + state = _emergencyProtection.getEmergencyState(); + + assertEq(state.activationCommittee, address(0x1)); + assertEq(state.executionCommittee, address(0x2)); + assertEq(state.protectedTill, block.timestamp + 100); + assertEq(state.emergencyModeDuration, 200); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + + _emergencyProtection.activate(); + + state = _emergencyProtection.getEmergencyState(); + + assertEq(state.activationCommittee, address(0x1)); + assertEq(state.executionCommittee, address(0x2)); + assertEq(state.protectedTill, block.timestamp + 100); + assertEq(state.emergencyModeDuration, 200); + assertEq(state.emergencyModeEndsAfter, block.timestamp + 200); + assertEq(state.isEmergencyModeActivated, true); + + _emergencyProtection.deactivate(); + + state = _emergencyProtection.getEmergencyState(); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + function test_is_emergency_mode_activated() external { + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + + _emergencyProtection.activate(); + + assertEq(_emergencyProtection.isEmergencyModeActivated(), true); + + _emergencyProtection.deactivate(); + + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + } + + function test_is_emergency_mode_passed() external { + assertEq(_emergencyProtection.isEmergencyModePassed(), false); + + uint256 duration = 200; + + _emergencyProtection.setup(address(0x1), address(0x2), 100, duration); + + assertEq(_emergencyProtection.isEmergencyModePassed(), false); + + _emergencyProtection.activate(); + + assertEq(_emergencyProtection.isEmergencyModePassed(), false); + + _wait(duration + 1); + + assertEq(_emergencyProtection.isEmergencyModePassed(), true); + + _emergencyProtection.deactivate(); + + assertEq(_emergencyProtection.isEmergencyModePassed(), false); + } + + function test_is_emergency_protection_enabled() external { + uint256 protectedTill = 100; + uint256 duration = 200; + + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + + _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, duration); + + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + + _wait(protectedTill - block.timestamp); + + EmergencyProtection.activate(_emergencyProtection); + + _wait(duration); + + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + + _wait(100); + + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + + EmergencyProtection.deactivate(_emergencyProtection); + + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + } + + function testFuzz_check_activation_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); + _emergencyProtection.checkActivationCommittee(stranger); + _emergencyProtection.checkActivationCommittee(address(0)); + + _emergencyProtection.setup(committee, address(0x2), 100, 100); + + _emergencyProtection.checkActivationCommittee(committee); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); + _emergencyProtection.checkActivationCommittee(stranger); + } + + function testFuzz_check_execution_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); + _emergencyProtection.checkExecutionCommittee(stranger); + _emergencyProtection.checkExecutionCommittee(address(0)); + + _emergencyProtection.setup(address(0x1), committee, 100, 100); + + _emergencyProtection.checkExecutionCommittee(committee); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); + _emergencyProtection.checkExecutionCommittee(stranger); + } + + function test_check_emergency_mode_active() external { + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _emergencyProtection.checkEmergencyModeActive(true); + _emergencyProtection.checkEmergencyModeActive(false); + + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + _emergencyProtection.activate(); + + _emergencyProtection.checkEmergencyModeActive(true); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) + ); + } +} diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol new file mode 100644 index 00000000..3b4ed280 --- /dev/null +++ b/test/unit/libraries/Proposals.t.sol @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Test.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {Proposals, ExecutorCall, Proposal, Status} from "contracts/libraries/Proposals.sol"; + +import {TargetMock} from "test/utils/utils.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; + +contract ProposalsUnitTests is UnitTest { + using Proposals for Proposals.State; + + TargetMock private _targetMock; + Proposals.State internal _proposals; + Executor private _executor; + + uint256 private constant PROPOSAL_ID_OFFSET = 1; + + function setUp() external { + _targetMock = new TargetMock(); + _executor = new Executor(address(this)); + } + + function test_submit_reverts_if_empty_proposals() external { + vm.expectRevert(Proposals.EmptyCalls.selector); + Proposals.submit(_proposals, address(0), new ExecutorCall[](0)); + } + + function test_submit_proposal() external { + uint256 proposalsCount = _proposals.count(); + + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); + + vm.expectEmit(); + emit Proposals.ProposalSubmitted(proposalsCount + PROPOSAL_ID_OFFSET, address(_executor), calls); + + vm.recordLogs(); + + Proposals.submit(_proposals, address(_executor), calls); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + + proposalsCount = _proposals.count(); + + Proposals.ProposalPacked memory proposal = _proposals.proposals[proposalsCount - PROPOSAL_ID_OFFSET]; + + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, block.timestamp); + assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.calls.length, 1); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function testFuzz_schedule_proposal(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + + uint256 proposalId = _proposals.count(); + + _wait(delay); + + vm.expectEmit(); + emit Proposals.ProposalScheduled(proposalId); + Proposals.schedule(_proposals, proposalId, delay); + + proposal = _proposals.proposals[0]; + + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, block.timestamp); + assertEq(proposal.executedAt, 0); + } + + function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function test_cannot_schedule_proposal_twice() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = 1; + Proposals.schedule(_proposals, proposalId, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function testFuzz_cannot_schedule_proposal_before_delay_passed(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + + _wait(delay - 1); + + vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET)); + Proposals.schedule(_proposals, PROPOSAL_ID_OFFSET, delay); + } + + function test_cannot_schedule_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + Proposals.cancelAll(_proposals); + + uint256 proposalId = _proposals.count(); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function testFuzz_execute_proposal(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + uint256 submittedAndScheduledAt = block.timestamp; + + assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].executedAt, 0); + + _wait(delay); + + // TODO: figure out why event is not emitted + // vm.expectEmit(); + // emit Proposals.ProposalExecuted(); + Proposals.execute(_proposals, proposalId, delay); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + + Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; + + assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); + assertEq(proposal.executedAt, block.timestamp); + } + + function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_unscheduled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_twice() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + Proposals.cancelAll(_proposals); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function testFuzz_cannot_execute_before_delay_passed(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + _wait(delay - 1); + + vm.expectRevert(abi.encodeWithSelector(Proposals.AfterScheduleDelayNotPassed.selector, proposalId)); + Proposals.execute(_proposals, proposalId, delay); + } + + function test_cancel_all_proposals() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + + uint256 proposalsCount = _proposals.count(); + + Proposals.schedule(_proposals, proposalsCount, 0); + + vm.expectEmit(); + emit Proposals.ProposalsCancelledTill(proposalsCount); + Proposals.cancelAll(_proposals); + + assertEq(_proposals.lastCancelledProposalId, proposalsCount); + } + + function test_get_proposal() external { + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); + Proposals.submit(_proposals, address(_executor), calls); + uint256 proposalId = _proposals.count(); + + Proposal memory proposal = _proposals.get(proposalId); + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Submitted); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.schedule(_proposals, proposalId, 0); + + uint256 scheduledAt = block.timestamp; + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, scheduledAt); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Scheduled); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.execute(_proposals, proposalId, 0); + + uint256 executedAt = block.timestamp; + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, scheduledAt); + assertEq(proposal.executedAt, executedAt); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Executed); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function test_get_cancelled_proposal() external { + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); + Proposals.submit(_proposals, address(_executor), calls); + uint256 proposalId = _proposals.count(); + + Proposal memory proposal = _proposals.get(proposalId); + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Submitted); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.cancelAll(_proposals); + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Cancelled); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function testFuzz_get_not_existing_proposal(uint256 proposalId) external { + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotFound.selector, proposalId)); + _proposals.get(proposalId); + } + + function test_count_proposals() external { + assertEq(_proposals.count(), 0); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.count(), 1); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.count(), 2); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.count(), 3); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.count(), 4); + + Proposals.schedule(_proposals, 1, 0); + assertEq(_proposals.count(), 4); + + Proposals.schedule(_proposals, 2, 0); + assertEq(_proposals.count(), 4); + + Proposals.execute(_proposals, 1, 0); + assertEq(_proposals.count(), 4); + + Proposals.cancelAll(_proposals); + assertEq(_proposals.count(), 4); + } + + function test_can_execute_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + + assert(!_proposals.canExecute(proposalId, 0)); + + Proposals.schedule(_proposals, proposalId, 0); + + assert(!_proposals.canExecute(proposalId, 100)); + + _wait(100); + + assert(_proposals.canExecute(proposalId, 100)); + + Proposals.execute(_proposals, proposalId, 0); + + assert(!_proposals.canExecute(proposalId, 100)); + } + + function test_can_not_execute_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + assert(_proposals.canExecute(proposalId, 0)); + Proposals.cancelAll(_proposals); + + assert(!_proposals.canExecute(proposalId, 0)); + } + + function test_can_schedule_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + assert(!_proposals.canSchedule(proposalId, 100)); + + _wait(100); + + assert(_proposals.canSchedule(proposalId, 100)); + + Proposals.schedule(_proposals, proposalId, 100); + Proposals.execute(_proposals, proposalId, 0); + + assert(!_proposals.canSchedule(proposalId, 100)); + } + + function test_can_not_schedule_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + uint256 proposalId = _proposals.count(); + assert(_proposals.canSchedule(proposalId, 0)); + + Proposals.cancelAll(_proposals); + + assert(!_proposals.canSchedule(proposalId, 0)); + } +} diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol new file mode 100644 index 00000000..1ff6f631 --- /dev/null +++ b/test/unit/mocks/TimelockMock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; + +contract TimelockMock { + uint8 public constant OFFSET = 1; + + mapping(uint256 => bool) public canScheduleProposal; + + uint256[] public submittedProposals; + uint256[] public scheduledProposals; + uint256[] public executedProposals; + + uint256 public lastCancelledProposalId; + + function submit(address, ExecutorCall[] calldata) external returns (uint256 newProposalId) { + newProposalId = submittedProposals.length + OFFSET; + submittedProposals.push(newProposalId); + canScheduleProposal[newProposalId] = false; + return newProposalId; + } + function schedule(uint256 proposalId) external { + if (canScheduleProposal[proposalId] == false) { + revert(); + } + + scheduledProposals.push(proposalId); + } + function execute(uint256 proposalId) external { + executedProposals.push(proposalId); + } + function canSchedule(uint256 proposalId) external view returns (bool) { + return canScheduleProposal[proposalId]; + } + function cancelAllNonExecutedProposals() external { + lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; + } + function setSchedule(uint256 proposalId) external { + canScheduleProposal[proposalId] = true; + } + + function getSubmittedProposals() external view returns (uint256[] memory) { + return submittedProposals; + } + function getScheduledProposals() external view returns (uint256[] memory) { + return scheduledProposals; + } + function getExecutedProposals() external view returns (uint256[] memory) { + return executedProposals; + } + function getLastCancelledProposalId() external view returns (uint256) { + return lastCancelledProposalId; + } +} \ No newline at end of file diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 2d3c56f3..40cd495f 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -106,3 +106,9 @@ interface IWithdrawalQueue is IERC721 { function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); } + +interface IDangerousContract { + function doRegularStaff(uint256 magic) external; + function doRugPool() external; + function doControversialStaff() external; +} \ No newline at end of file diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 1565afec..fabbb898 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -26,7 +26,7 @@ import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernan import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; import {Percents, percents} from "../utils/percents.sol"; -import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus} from "../utils/interfaces.sol"; +import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus, IDangerousContract} from "../utils/interfaces.sol"; import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; @@ -47,12 +47,6 @@ function countDigits(uint256 number) pure returns (uint256 digitsCount) { } while (number / 10 != 0); } -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} - contract ScenarioTestBlueprint is Test { address internal immutable _ADMIN_PROPOSER = DAO_VOTING; uint256 internal immutable _EMERGENCY_MODE_DURATION = 180 days; @@ -379,7 +373,7 @@ contract ScenarioTestBlueprint is Test { } function _assertProposalCanceled(uint256 proposalId) internal { - assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Canceled, "Proposal not in 'Canceled' state"); + assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); } function _assertNormalState() internal { diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol new file mode 100644 index 00000000..6ff279a4 --- /dev/null +++ b/test/utils/unit-test.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// solhint-disable-next-line +import {Test} from "forge-std/Test.sol"; +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; + +contract UnitTest is Test { + function _wait(uint256 duration) internal { + vm.warp(block.timestamp + duration); + } + + function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExecutorCall[] memory) { + return ExecutorCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); + } +} \ No newline at end of file