Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PolicyEnabler mix-in #40

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/policies/utils/PolicyEnabler.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}
9 changes: 9 additions & 0 deletions src/policies/utils/RoleDefinitions.sol
Original file line number Diff line number Diff line change
@@ -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";
90 changes: 90 additions & 0 deletions src/test/policies/utils/PolicyEnabler/MockPolicyEnabler.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
118 changes: 118 additions & 0 deletions src/test/policies/utils/PolicyEnabler/PolicyEnablerTest.sol
Original file line number Diff line number Diff line change
@@ -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"
);
}
}
Loading
Loading