diff --git a/src/policies/utils/PolicyEnabler.sol b/src/policies/utils/PolicyEnabler.sol new file mode 100644 index 00000000..739cacf8 --- /dev/null +++ b/src/policies/utils/PolicyEnabler.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {RolesConsumer} from "src/modules/ROLES/OlympusRoles.sol"; +import {ADMIN_ROLE, EMERGENCY_ROLE} from "./RoleDefinitions.sol"; + +/// @title PolicyEnabler +/// @notice This contract is designed to be inherited by contracts that need to be enabled or disabled. It replaces the inconsistent usage of `active` and `locallyActive` state variables across the codebase. +/// @dev A contract that inherits from this contract should use the `onlyEnabled` and `onlyDisabled` modifiers to gate access to certain functions. +/// +/// Inheriting contracts must do the following: +/// - In `configureDependencies()`, assign the module address to the `ROLES` state variable, e.g. `ROLES = ROLESv1(getModuleAddress(toKeycode("ROLES")));` +/// +/// The following are optional: +/// - Override the `_enable()` and `_disable()` functions if custom logic and/or parameters are needed for the enable/disable functions. +/// - For example, `enable()` could be called with initialisation data that is decoded, validated and assigned in `_enable()`. +abstract contract PolicyEnabler is RolesConsumer { + // ===== STATE VARIABLES ===== // + + /// @notice Whether the policy functionality is enabled + bool public isEnabled; + + // ===== ERRORS ===== // + + error NotAuthorised(); + error NotDisabled(); + error NotEnabled(); + + // ===== EVENTS ===== // + + event Disabled(); + event Enabled(); + + // ===== MODIFIERS ===== // + + /// @notice Modifier that reverts if the caller does not have the emergency or admin role + modifier onlyEmergencyOrAdminRole() { + if (!ROLES.hasRole(msg.sender, EMERGENCY_ROLE) && !ROLES.hasRole(msg.sender, ADMIN_ROLE)) + revert NotAuthorised(); + _; + } + + /// @notice Modifier that reverts if the policy is not enabled + modifier onlyEnabled() { + if (!isEnabled) revert NotEnabled(); + _; + } + + /// @notice Modifier that reverts if the policy is enabled + modifier onlyDisabled() { + if (isEnabled) revert NotDisabled(); + _; + } + + // ===== ENABLEABLE FUNCTIONS ===== // + + /// @notice Enable the contract + /// @dev This function performs the following steps: + /// 1. Validates that the caller has `ROLE` ("emergency_shutdown") + /// 2. Validates that the contract is disabled + /// 3. Calls the implementation-specific `_enable()` function + /// 4. Changes the state of the contract to enabled + /// 5. Emits the `Enabled` event + /// + /// @param enableData_ The data to pass to the implementation-specific `_enable()` function + function enable(bytes calldata enableData_) public onlyEmergencyOrAdminRole onlyDisabled { + // Call the implementation-specific enable function + _enable(enableData_); + + // Change the state + isEnabled = true; + + // Emit the enabled event + emit Enabled(); + } + + /// @notice Implementation-specific enable function + /// @dev This function is called by the `enable()` function + /// + /// The implementing contract can override this function and perform the following: + /// 1. Validate any parameters (if needed) or revert + /// 2. Validate state (if needed) or revert + /// 3. Perform any necessary actions, apart from modifying the `isEnabled` state variable + /// + /// @param enableData_ Custom data that can be used by the implementation. The format of this data is + /// left to the discretion of the implementation. + function _enable(bytes calldata enableData_) internal virtual {} + + /// @notice Disable the contract + /// @dev This function performs the following steps: + /// 1. Validates that the caller has `ROLE` ("emergency_shutdown") + /// 2. Validates that the contract is enabled + /// 3. Calls the implementation-specific `_disable()` function + /// 4. Changes the state of the contract to disabled + /// 5. Emits the `Disabled` event + /// + /// @param disableData_ The data to pass to the implementation-specific `_disable()` function + function disable(bytes calldata disableData_) public onlyEmergencyOrAdminRole onlyEnabled { + // Call the implementation-specific disable function + _disable(disableData_); + + // Change the state + isEnabled = false; + + // Emit the disabled event + emit Disabled(); + } + + /// @notice Implementation-specific disable function + /// @dev This function is called by the `disable()` function. + /// + /// The implementing contract can override this function and perform the following: + /// 1. Validate any parameters (if needed) or revert + /// 2. Validate state (if needed) or revert + /// 3. Perform any necessary actions, apart from modifying the `isEnabled` state variable + /// + /// @param disableData_ Custom data that can be used by the implementation. The format of this data is + /// left to the discretion of the implementation. + function _disable(bytes calldata disableData_) internal virtual {} +} diff --git a/src/policies/utils/RoleDefinitions.sol b/src/policies/utils/RoleDefinitions.sol new file mode 100644 index 00000000..0ab07166 --- /dev/null +++ b/src/policies/utils/RoleDefinitions.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @dev Allows enabling/disabling the protocol/policies in an emergency +bytes32 constant EMERGENCY_ROLE = "emergency"; +/// @dev Administrative access, e.g. configuration parameters. Typically assigned to on-chain governance. +bytes32 constant ADMIN_ROLE = "admin"; +/// @dev Managerial access, e.g. managing specific protocol parameters. Typically assigned to a multisig/council. +bytes32 constant MANAGER_ROLE = "manager"; diff --git a/src/test/policies/utils/PolicyEnabler/MockPolicyEnabler.sol b/src/test/policies/utils/PolicyEnabler/MockPolicyEnabler.sol new file mode 100644 index 00000000..859412fb --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/MockPolicyEnabler.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Unlicense +// solhint-disable one-contract-per-file +pragma solidity 0.8.15; + +import {console2} from "forge-std/console2.sol"; + +import {Kernel, Keycode, Policy, toKeycode} from "src/Kernel.sol"; +import {ROLESv1} from "src/modules/ROLES/ROLES.v1.sol"; +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; + +/// @notice Mock Policy that can be enabled and disabled. +/// @dev This contract does not implement any custom enable/disable logic. +contract MockPolicyEnabler is Policy, PolicyEnabler { + uint256 public enableValue; + uint256 public disableValue; + uint256 public disableAnotherValue; + + constructor(Kernel kernel_) Policy(kernel_) {} + + function configureDependencies() external override returns (Keycode[] memory dependencies) { + dependencies = new Keycode[](1); + dependencies[0] = toKeycode("ROLES"); + + ROLES = ROLESv1(getModuleAddress(dependencies[0])); + + return dependencies; + } + + function requiresEnabled() external view onlyEnabled returns (bool) { + return true; + } + + function requiresDisabled() external view onlyDisabled returns (bool) { + return true; + } +} + +/// @notice Mock Policy that can be enabled and disabled. +/// @dev This contract implements custom enable/disable logic. +contract MockPolicyEnablerWithCustomLogic is MockPolicyEnabler { + // Define a structure for the enable data + struct EnableData { + uint256 value; + } + + struct DisableData { + uint256 value; + uint256 anotherValue; + } + + bool public enableShouldRevert; + bool public disableShouldRevert; + + constructor(Kernel kernel_) MockPolicyEnabler(kernel_) {} + + function setEnableShouldRevert(bool shouldRevert_) external { + enableShouldRevert = shouldRevert_; + } + + function setDisableShouldRevert(bool shouldRevert_) external { + disableShouldRevert = shouldRevert_; + } + + function _enable(bytes calldata data_) internal override { + // Decode the enable data + EnableData memory enableData = abi.decode(data_, (EnableData)); + + // Log the enable data + console2.log("Enable data:", enableData.value); + + // solhint-disable-next-line custom-errors + if (enableShouldRevert) revert("Enable should revert"); + + enableValue = enableData.value; + } + + function _disable(bytes calldata data_) internal override { + // Decode the disable data + DisableData memory disableData = abi.decode(data_, (DisableData)); + + // Log the disable data + console2.log("Disable data:", disableData.value, disableData.anotherValue); + + // solhint-disable-next-line custom-errors + if (disableShouldRevert) revert("Disable should revert"); + + disableValue = disableData.value; + disableAnotherValue = disableData.anotherValue; + } +} diff --git a/src/test/policies/utils/PolicyEnabler/PolicyEnablerTest.sol b/src/test/policies/utils/PolicyEnabler/PolicyEnablerTest.sol new file mode 100644 index 00000000..d47365cc --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/PolicyEnablerTest.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {Kernel, Actions} from "src/Kernel.sol"; + +import {OlympusRoles} from "src/modules/ROLES/OlympusRoles.sol"; +import {RolesAdmin} from "src/policies/RolesAdmin.sol"; + +import {ADMIN_ROLE, EMERGENCY_ROLE} from "src/policies/utils/RoleDefinitions.sol"; + +import {MockPolicyEnabler, MockPolicyEnablerWithCustomLogic} from "./MockPolicyEnabler.sol"; + +contract PolicyEnablerTest is Test { + address public constant EMERGENCY = address(0xAAAA); + address public constant ADMIN = address(0xBBBB); + + Kernel public kernel; + OlympusRoles public roles; + RolesAdmin public rolesAdmin; + MockPolicyEnabler public policyEnabler; + MockPolicyEnablerWithCustomLogic public policyEnablerWithCustomLogic; + + uint256 public enableValue; + uint256 public disableValue; + uint256 public disableAnotherValue; + + bytes public enableData; + bytes public disableData; + + function setUp() public { + kernel = new Kernel(); + roles = new OlympusRoles(kernel); + rolesAdmin = new RolesAdmin(kernel); + + policyEnabler = new MockPolicyEnabler(kernel); + + // Install + kernel.executeAction(Actions.InstallModule, address(roles)); + kernel.executeAction(Actions.ActivatePolicy, address(rolesAdmin)); + kernel.executeAction(Actions.ActivatePolicy, address(policyEnabler)); + + // Grant roles + rolesAdmin.grantRole(ADMIN_ROLE, ADMIN); + rolesAdmin.grantRole(EMERGENCY_ROLE, EMERGENCY); + } + + modifier givenPolicyHasCustomLogic() { + policyEnabler = new MockPolicyEnablerWithCustomLogic(kernel); + + kernel.executeAction(Actions.ActivatePolicy, address(policyEnabler)); + + _; + } + + modifier givenPolicyEnableCustomLogicReverts() { + (MockPolicyEnablerWithCustomLogic(address(policyEnabler))).setEnableShouldRevert(true); + _; + } + + modifier givenPolicyDisableCustomLogicReverts() { + (MockPolicyEnablerWithCustomLogic(address(policyEnabler))).setDisableShouldRevert(true); + _; + } + + modifier givenEnabled() { + vm.prank(EMERGENCY); + policyEnabler.enable(enableData); + _; + } + + modifier givenDisabled() { + vm.prank(EMERGENCY); + policyEnabler.disable(disableData); + _; + } + + modifier givenEnableData(uint256 value_) { + enableValue = value_; + + enableData = abi.encode(MockPolicyEnablerWithCustomLogic.EnableData({value: value_})); + _; + } + + modifier givenDisableData(uint256 value_, uint256 anotherValue_) { + disableValue = value_; + disableAnotherValue = anotherValue_; + + disableData = abi.encode( + MockPolicyEnablerWithCustomLogic.DisableData({ + value: value_, + anotherValue: anotherValue_ + }) + ); + _; + } + + function _assertStateVariables( + bool isEnabled_, + uint256 expectedEnableValue_, + uint256 expectedDisableValue_, + uint256 expectedDisableAnotherValue_ + ) internal { + // Assert enabled + assertEq(policyEnabler.isEnabled(), isEnabled_, "isEnabled"); + + // Enable + assertEq(policyEnabler.enableValue(), expectedEnableValue_, "enableValue"); + + // Disable + assertEq(policyEnabler.disableValue(), expectedDisableValue_, "disableValue"); + assertEq( + policyEnabler.disableAnotherValue(), + expectedDisableAnotherValue_, + "disableAnotherValue" + ); + } +} diff --git a/src/test/policies/utils/PolicyEnabler/disable.t.sol b/src/test/policies/utils/PolicyEnabler/disable.t.sol new file mode 100644 index 00000000..9256c928 --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/disable.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.15; + +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; +import {PolicyEnablerTest} from "./PolicyEnablerTest.sol"; + +contract PolicyEnablerDisableTest is PolicyEnablerTest { + event Disabled(); + + // given the caller does not have the emergency or admin roles + // [X] it reverts + // given the policy is disabled + // [X] it reverts + // given the caller has the admin role + // [X] it sets the enabled flag to false + // [X] it emits the Disabled event + // given the caller has the emergency role + // [X] it sets the enabled flag to false + // [X] it emits the Disabled event + // given the policy has custom disable logic + // given the custom disable logic reverts + // [ X] it reverts + // [X] it calls the implementation-specific disable function + // [X] it sets the enabled flag to false + // [X] it emits the Disabled event + + function test_callerNotEmergencyOrAdminRole_reverts(address caller_) public { + vm.assume(caller_ != EMERGENCY && caller_ != ADMIN); + + // Expect revert + vm.expectRevert(PolicyEnabler.NotAuthorised.selector); + + // Call function + vm.prank(caller_); + policyEnabler.disable(disableData); + } + + function test_policyDisabled_reverts() public { + // Expect revert + vm.expectRevert(PolicyEnabler.NotEnabled.selector); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.disable(disableData); + } + + function test_callerHasAdminRole() public givenEnabled { + // Expect event + vm.expectEmit(); + emit Disabled(); + + // Call function + vm.prank(ADMIN); + policyEnabler.disable(disableData); + + // Assert state + _assertStateVariables(false, 0, 0, 0); + } + + function test_callerHasEmergencyRole() public givenEnabled { + // Expect event + vm.expectEmit(); + emit Disabled(); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.disable(disableData); + + // Assert state + _assertStateVariables(false, 0, 0, 0); + } + + function test_customLogic_reverts() + public + givenPolicyHasCustomLogic + givenPolicyDisableCustomLogicReverts + givenEnableData(5) + givenEnabled + givenDisableData(1, 2) + { + // Expect revert + vm.expectRevert("Disable should revert"); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.disable(disableData); + } + + function test_customLogic( + uint256 value_, + uint256 anotherValue_ + ) + public + givenPolicyHasCustomLogic + givenEnableData(5) + givenEnabled + givenDisableData(value_, anotherValue_) + { + // Expect event + vm.expectEmit(); + emit Disabled(); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.disable(disableData); + + // Assert state + _assertStateVariables(false, 5, value_, anotherValue_); + } +} diff --git a/src/test/policies/utils/PolicyEnabler/enable.t.sol b/src/test/policies/utils/PolicyEnabler/enable.t.sol new file mode 100644 index 00000000..cd3f1fe8 --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/enable.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.15; + +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; +import {PolicyEnablerTest} from "./PolicyEnablerTest.sol"; + +contract PolicyEnablerEnableTest is PolicyEnablerTest { + event Enabled(); + + // given the caller does not have the emergency or admin roles + // [X] it reverts + // given the policy is enabled + // [X] it reverts + // given the caller has the admin role + // [X] it sets the enabled flag to true + // [X] it emits the Enabled event + // given the caller has the emergency role + // [X] it sets the enabled flag to true + // [X] it emits the Enabled event + // given the policy has custom enable logic + // given the custom enable logic reverts + // [X] it reverts + // [X] it calls the implementation-specific enable function + // [X] it sets the enabled flag to true + // [X] it emits the Enabled event + + function test_callerNotEmergencyOrAdminRole_reverts(address caller_) public { + vm.assume(caller_ != EMERGENCY && caller_ != ADMIN); + + // Expect revert + vm.expectRevert(PolicyEnabler.NotAuthorised.selector); + + // Call function + vm.prank(caller_); + policyEnabler.enable(enableData); + } + + function test_policyEnabled_reverts() public givenEnabled { + // Expect revert + vm.expectRevert(PolicyEnabler.NotDisabled.selector); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.enable(enableData); + } + + function test_callerHasAdminRole() public { + // Expect event + vm.expectEmit(); + emit Enabled(); + + // Call function + vm.prank(ADMIN); + policyEnabler.enable(enableData); + + // Assert state + _assertStateVariables(true, 0, 0, 0); + } + + function test_callerHasEmergencyRole() public { + // Expect event + vm.expectEmit(); + emit Enabled(); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.enable(enableData); + + // Assert state + _assertStateVariables(true, 0, 0, 0); + } + + function test_customLogic_reverts() + public + givenPolicyHasCustomLogic + givenPolicyEnableCustomLogicReverts + givenEnableData(1) + { + // Expect revert + vm.expectRevert("Enable should revert"); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.enable(enableData); + } + + function test_customLogic( + uint256 value_ + ) public givenPolicyHasCustomLogic givenEnableData(value_) { + // Expect event + vm.expectEmit(); + emit Enabled(); + + // Call function + vm.prank(EMERGENCY); + policyEnabler.enable(enableData); + + // Assert state + _assertStateVariables(true, value_, 0, 0); + } +} diff --git a/src/test/policies/utils/PolicyEnabler/onlyDisabled.t.sol b/src/test/policies/utils/PolicyEnabler/onlyDisabled.t.sol new file mode 100644 index 00000000..c413c4e8 --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/onlyDisabled.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.15; + +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; +import {PolicyEnablerTest} from "./PolicyEnablerTest.sol"; + +contract PolicyEnablerOnlyDisabledTest is PolicyEnablerTest { + // given the policy is enabled + // [ ] it reverts + // [ ] it does not revert +} diff --git a/src/test/policies/utils/PolicyEnabler/onlyEnabled.t.sol b/src/test/policies/utils/PolicyEnabler/onlyEnabled.t.sol new file mode 100644 index 00000000..daed4d0f --- /dev/null +++ b/src/test/policies/utils/PolicyEnabler/onlyEnabled.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.15; + +import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol"; +import {PolicyEnablerTest} from "./PolicyEnablerTest.sol"; + +contract PolicyEnablerOnlyEnabledTest is PolicyEnablerTest { + // given the policy is disabled + // [ ] it reverts + // [ ] it does not revert +}