diff --git a/contracts/acl/ACLSyntaxSugar.sol b/contracts/acl/ACLSyntaxSugar.sol index f5683c0bf..e4aa57ed5 100644 --- a/contracts/acl/ACLSyntaxSugar.sol +++ b/contracts/acl/ACLSyntaxSugar.sol @@ -18,14 +18,42 @@ contract ACLSyntaxSugar { return arr(uint256(_a), uint256(_b)); } + function arr(bytes32 _a, address _b) internal pure returns (uint256[] r) { + return arr(uint256(_a), uint256(_b)); + } + + function arr(bytes32 _a, address _b, address _c) internal pure returns (uint256[] r) { + return arr(uint256(_a), uint256(_b), uint256(_c)); + } + + function arr(bytes32 _a, uint256 _b) internal pure returns (uint256[] r) { + return arr(uint256(_a), _b); + } + + function arr(bytes32 _a, uint256 _b, uint256 _c) internal pure returns (uint256[] r) { + return arr(uint256(_a), _b, _c); + } + function arr(address _a) internal pure returns (uint256[] r) { return arr(uint256(_a)); } + function arr(address _a, bool _b) internal pure returns (uint256[] r) { + return arr(uint256(_a), _toUint(_b)); + } + + function arr(address _a, bool _b, bool _c) internal pure returns (uint256[] r) { + return arr(uint256(_a), _toUint(_b), _toUint(_c)); + } + function arr(address _a, address _b) internal pure returns (uint256[] r) { return arr(uint256(_a), uint256(_b)); } + function arr(address _a, uint256 _b) internal pure returns (uint256[] r) { + return arr(uint256(_a), _b); + } + function arr(address _a, uint256 _b, uint256 _c) internal pure returns (uint256[] r) { return arr(uint256(_a), _b, _c); } @@ -34,10 +62,6 @@ contract ACLSyntaxSugar { return arr(uint256(_a), _b, _c, _d); } - function arr(address _a, uint256 _b) internal pure returns (uint256[] r) { - return arr(uint256(_a), uint256(_b)); - } - function arr(address _a, address _b, uint256 _c, uint256 _d, uint256 _e) internal pure returns (uint256[] r) { return arr(uint256(_a), uint256(_b), _c, _d, _e); } @@ -84,6 +108,10 @@ contract ACLSyntaxSugar { r[3] = _d; r[4] = _e; } + + function _toUint(bool _a) private pure returns (uint256) { + return _a ? uint256(1) : uint256(0); + } } diff --git a/contracts/apps/AragonApp.sol b/contracts/apps/AragonApp.sol index f53c40721..b49488aa0 100644 --- a/contracts/apps/AragonApp.sol +++ b/contracts/apps/AragonApp.sol @@ -40,12 +40,8 @@ contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGua * Always returns false if the app hasn't been initialized yet. */ function canPerform(address _sender, bytes32 _role, uint256[] _params) public view returns (bool) { - if (!hasInitialized()) { - return false; - } - IKernel linkedKernel = kernel(); - if (address(linkedKernel) == address(0)) { + if (address(linkedKernel) == address(0) || !isCallEnabled()) { return false; } @@ -57,6 +53,47 @@ contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGua ); } + /** + * @dev Check whether a call to the current app can be executed or not based on the kill-switch settings + * @return Boolean indicating whether the call could be executed or not + */ + function isCallEnabled() public view returns (bool) { + if (!hasInitialized()) { + return false; + } + + IKernel _kernel = kernel(); + bytes4 selector = _kernel.isAppDisabled.selector; + bytes memory isAppDisabledCalldata = abi.encodeWithSelector(selector, appId(), address(this)); + bool success; + assembly { + success := staticcall(gas, _kernel, add(isAppDisabledCalldata, 0x20), mload(isAppDisabledCalldata), 0, 0) + } + + // If the call to `kernel.isAppDisabled()` reverts (using an old or non-existent Kernel) we consider that + // there is no kill switch. Therefore, the the call can be executed. + if (!success) { + return true; + } + + // If it does not revert, check if the returned value is 32-bytes length, otherwise return false + uint256 _outputLength; + assembly { _outputLength := returndatasize } + if (_outputLength != 32) { + return false; + } + + // Forward returned value + bool _shouldDenyCall; + assembly { + let ptr := mload(0x40) // get next free memory pointer + mstore(0x40, add(ptr, 0x20)) // set next free memory pointer + returndatacopy(ptr, 0, 0x20) // copy call return value + _shouldDenyCall := mload(ptr) // read data + } + return !_shouldDenyCall; + } + /** * @dev Get the recovery vault for the app * @return Recovery vault address for the app diff --git a/contracts/factory/DAOFactory.sol b/contracts/factory/DAOFactory.sol index a2eec709a..161869714 100644 --- a/contracts/factory/DAOFactory.sol +++ b/contracts/factory/DAOFactory.sol @@ -1,33 +1,48 @@ pragma solidity 0.4.24; +import "../acl/IACL.sol"; +import "../acl/ACL.sol"; import "../kernel/IKernel.sol"; import "../kernel/Kernel.sol"; import "../kernel/KernelProxy.sol"; - -import "../acl/IACL.sol"; -import "../acl/ACL.sol"; - +import "../kill-switch/KillSwitch.sol"; +import "../kill-switch/IssuesRegistry.sol"; import "./EVMScriptRegistryFactory.sol"; contract DAOFactory { + string private constant ERROR_MISSING_BASE_KILL_SWITCH = "DF_MISSING_BASE_KILL_SWITCH"; + IKernel public baseKernel; IACL public baseACL; + KillSwitch public baseKillSwitch; EVMScriptRegistryFactory public regFactory; event DeployDAO(address dao); - event DeployEVMScriptRegistry(address reg); + event DeployKillSwitch(address killSwitch); + event DeployEVMScriptRegistry(address registry); /** - * @notice Create a new DAOFactory, creating DAOs with Kernels proxied to `_baseKernel`, ACLs proxied to `_baseACL`, and new EVMScriptRegistries created from `_regFactory`. + * @notice Create a new DAOFactory, creating DAOs with Kernels proxied to `_baseKernel`, ACLs proxied to `_baseACL`, and new EVMScriptRegistries created from `_scriptsRegistryFactory`. * @param _baseKernel Base Kernel * @param _baseACL Base ACL - * @param _regFactory EVMScriptRegistry factory + * @param _baseKillSwitch Base KillSwitch + * @param _scriptsRegistryFactory EVMScriptRegistry factory */ - constructor(IKernel _baseKernel, IACL _baseACL, EVMScriptRegistryFactory _regFactory) public { + constructor( + IKernel _baseKernel, + IACL _baseACL, + KillSwitch _baseKillSwitch, + EVMScriptRegistryFactory _scriptsRegistryFactory + ) + public + { // No need to init as it cannot be killed by devops199 - if (address(_regFactory) != address(0)) { - regFactory = _regFactory; + if (address(_scriptsRegistryFactory) != address(0)) { + regFactory = _scriptsRegistryFactory; + } + if (address(_baseKillSwitch) != address(0)) { + baseKillSwitch = _baseKillSwitch; } baseKernel = _baseKernel; @@ -40,38 +55,106 @@ contract DAOFactory { * @return Newly created DAO */ function newDAO(address _root) public returns (Kernel) { - Kernel dao = Kernel(new KernelProxy(baseKernel)); - if (address(regFactory) == address(0)) { - dao.initialize(baseACL, _root); - } else { - dao.initialize(baseACL, this); + return _createDAO(_root); + } - ACL acl = ACL(dao.acl()); - bytes32 permRole = acl.CREATE_PERMISSIONS_ROLE(); - bytes32 appManagerRole = dao.APP_MANAGER_ROLE(); + Kernel dao = _createDAO(address(this)); + ACL acl = ACL(dao.acl()); - acl.grantPermission(regFactory, acl, permRole); + // load roles + bytes32 appManagerRole = dao.APP_MANAGER_ROLE(); + bytes32 createPermissionsRole = acl.CREATE_PERMISSIONS_ROLE(); - acl.createPermission(regFactory, dao, appManagerRole, this); + // grant app manager permissions to factory and deploy EVM scripts registry + acl.createPermission(regFactory, dao, appManagerRole, address(this)); + _createEVMScriptRegistry(dao, acl, createPermissionsRole); - EVMScriptRegistry reg = regFactory.newEVMScriptRegistry(dao); - emit DeployEVMScriptRegistry(address(reg)); + // roll back app manager permissions + acl.revokePermission(regFactory, dao, appManagerRole); + acl.removePermissionManager(dao, appManagerRole); - // Clean up permissions - // First, completely reset the APP_MANAGER_ROLE - acl.revokePermission(regFactory, dao, appManagerRole); - acl.removePermissionManager(dao, appManagerRole); + // transfer create permissions roles to root address + acl.revokePermission(address(this), acl, createPermissionsRole); + acl.grantPermission(_root, acl, createPermissionsRole); + acl.setPermissionManager(_root, acl, createPermissionsRole); + + return dao; + } + + /** + * @notice Create a new DAO with `_root` set as the initial admin and `_issuesRegistry` as the source of truth for kill-switch purposes + * @param _root Address that will be granted control to setup DAO permissions + * @param _issuesRegistry Address of the registry of issues that will be used to detect critical situations by the kill switch + * @return Newly created DAO + */ + function newDAOWithKillSwitch(address _root, IssuesRegistry _issuesRegistry) public returns (Kernel) { + require(address(baseKillSwitch) != address(0), ERROR_MISSING_BASE_KILL_SWITCH); - // Then, make root the only holder and manager of CREATE_PERMISSIONS_ROLE - acl.revokePermission(regFactory, acl, permRole); - acl.revokePermission(this, acl, permRole); - acl.grantPermission(_root, acl, permRole); - acl.setPermissionManager(_root, acl, permRole); + Kernel dao = _createDAO(address(this)); + ACL acl = ACL(dao.acl()); + + // load roles + bytes32 appManagerRole = dao.APP_MANAGER_ROLE(); + bytes32 createPermissionsRole = acl.CREATE_PERMISSIONS_ROLE(); + + // grant app manager permissions to this and deploy kill switch + acl.createPermission(address(this), dao, appManagerRole, address(this)); + _createKillSwitch(dao, acl, _issuesRegistry); + + // deploy EVM scripts registry if required + if (address(regFactory) != address(0)) { + acl.grantPermission(regFactory, dao, appManagerRole); + _createEVMScriptRegistry(dao, acl, createPermissionsRole); + acl.revokePermission(regFactory, dao, appManagerRole); } - emit DeployDAO(address(dao)); + // roll back app manager permissions + acl.revokePermission(address(this), dao, appManagerRole); + acl.removePermissionManager(dao, appManagerRole); + + // transfer create permissions roles to root address + acl.revokePermission(address(this), acl, createPermissionsRole); + acl.grantPermission(_root, acl, createPermissionsRole); + acl.setPermissionManager(_root, acl, createPermissionsRole); return dao; } + + function _createDAO(address _permissionsCreator) internal returns (Kernel) { + Kernel dao = Kernel(new KernelProxy(baseKernel)); + dao.initialize(baseACL, _permissionsCreator); + emit DeployDAO(address(dao)); + return dao; + } + + function _createEVMScriptRegistry(Kernel _dao, ACL _acl, bytes32 _createPermissionsRole) internal { + _acl.grantPermission(regFactory, _acl, _createPermissionsRole); + EVMScriptRegistry scriptsRegistry = regFactory.newEVMScriptRegistry(_dao); + emit DeployEVMScriptRegistry(address(scriptsRegistry)); + _acl.revokePermission(regFactory, _acl, _createPermissionsRole); + } + + function _createKillSwitch(Kernel _dao, ACL _acl, IssuesRegistry _issuesRegistry) internal { + bytes32 killSwitchAppID = _dao.DEFAULT_KILL_SWITCH_APP_ID(); + bytes memory initializeData = abi.encodeWithSelector(baseKillSwitch.initialize.selector, _issuesRegistry); + KillSwitch killSwitch = KillSwitch(_dao.newAppInstance(killSwitchAppID, baseKillSwitch, initializeData, true)); + _allowKillSwitchCoreInstances(_dao, _acl, killSwitch); + emit DeployKillSwitch(address(killSwitch)); + } + + function _allowKillSwitchCoreInstances(Kernel _dao, ACL _acl, KillSwitch _killSwitch) internal { + // create change whitelisted instances role for this + bytes32 changeWhitelistedInstancesRole = _killSwitch.CHANGE_WHITELISTED_INSTANCES_ROLE(); + _acl.createPermission(address(this), _killSwitch, changeWhitelistedInstancesRole, address(this)); + + // whitelist core instances: kill switch, acl and kernel + _killSwitch.setWhitelistedInstance(address(_dao), true); + _killSwitch.setWhitelistedInstance(address(_acl), true); + _killSwitch.setWhitelistedInstance(address(_killSwitch), true); + + // revoke and remove change whitelisted instances role from this + _acl.revokePermission(address(this), _killSwitch, changeWhitelistedInstancesRole); + _acl.removePermissionManager(_killSwitch, changeWhitelistedInstancesRole); + } } diff --git a/contracts/kernel/IKernel.sol b/contracts/kernel/IKernel.sol index e1a2b40e5..dc5de6558 100644 --- a/contracts/kernel/IKernel.sol +++ b/contracts/kernel/IKernel.sol @@ -20,4 +20,5 @@ contract IKernel is IKernelEvents, IVaultRecoverable { function setApp(bytes32 namespace, bytes32 appId, address app) public; function getApp(bytes32 namespace, bytes32 appId) public view returns (address); + function isAppDisabled(bytes32 appId, address _instance) public view returns (bool); } diff --git a/contracts/kernel/Kernel.sol b/contracts/kernel/Kernel.sol index 1fc919055..bcbd47436 100644 --- a/contracts/kernel/Kernel.sol +++ b/contracts/kernel/Kernel.sol @@ -10,6 +10,7 @@ import "../common/IsContract.sol"; import "../common/Petrifiable.sol"; import "../common/VaultRecoverable.sol"; import "../factory/AppProxyFactory.sol"; +import "../kill-switch/IKillSwitch.sol"; import "../lib/misc/ERCProxy.sol"; @@ -20,9 +21,9 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant */ bytes32 public constant APP_MANAGER_ROLE = 0xb6d92708f3d4817afc106147d969e229ced5c46e65e0a5002a0d391287762bd0; + string private constant ERROR_AUTH_FAILED = "KERNEL_AUTH_FAILED"; string private constant ERROR_APP_NOT_CONTRACT = "KERNEL_APP_NOT_CONTRACT"; string private constant ERROR_INVALID_APP_CHANGE = "KERNEL_INVALID_APP_CHANGE"; - string private constant ERROR_AUTH_FAILED = "KERNEL_AUTH_FAILED"; /** * @dev Constructor that allows the deployer to choose if the base instance should be petrified immediately. @@ -162,6 +163,22 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant recoveryVaultAppId = _recoveryVaultAppId; } + /** + * @dev Tells whether a call to an instance of an app should be denied or not based on the kill-switch settings. + * Initialization check is implicitly provided by the KillSwitch's existence, as apps can only be installed after initialization. + * @param _appId Identifier for app to be checked + * @return True if the given call should be denied, false otherwise + */ + function isAppDisabled(bytes32 _appId, address _instance) public view returns (bool) { + IKillSwitch _killSwitch = killSwitch(); + if (address(_killSwitch) == address(0)) { + return false; + } + + address _baseApp = getApp(KERNEL_APP_BASES_NAMESPACE, _appId); + return _killSwitch.shouldDenyCallingApp(_appId, _baseApp, _instance); + } + // External access to default app id and namespace constants to mimic default getters for constants /* solium-disable function-order, mixedcase */ function CORE_NAMESPACE() external pure returns (bytes32) { return KERNEL_CORE_NAMESPACE; } @@ -169,6 +186,7 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant function APP_ADDR_NAMESPACE() external pure returns (bytes32) { return KERNEL_APP_ADDR_NAMESPACE; } function KERNEL_APP_ID() external pure returns (bytes32) { return KERNEL_CORE_APP_ID; } function DEFAULT_ACL_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_ACL_APP_ID; } + function DEFAULT_KILL_SWITCH_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_KILL_SWITCH_APP_ID; } /* solium-enable function-order, mixedcase */ /** @@ -190,13 +208,21 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant } /** - * @dev Get the installed ACL app + * @dev Get the default ACL app * @return ACL app */ function acl() public view returns (IACL) { return IACL(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_ACL_APP_ID)); } + /** + * @dev Get the default KillSwitch app + * @return KillSwitch app + */ + function killSwitch() public view returns (IKillSwitch) { + return IKillSwitch(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_KILL_SWITCH_APP_ID)); + } + /** * @dev Function called by apps to check ACL on kernel or to check permission status * @param _who Sender of the original call diff --git a/contracts/kernel/KernelConstants.sol b/contracts/kernel/KernelConstants.sol index 77816a74c..d9663b882 100644 --- a/contracts/kernel/KernelConstants.sol +++ b/contracts/kernel/KernelConstants.sol @@ -10,10 +10,12 @@ contract KernelAppIds { bytes32 internal constant KERNEL_CORE_APP_ID = apmNamehash("kernel"); bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = apmNamehash("acl"); bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("vault"); + bytes32 internal constant KERNEL_DEFAULT_KILL_SWITCH_APP_ID = apmNamehash("kill-switch"); */ bytes32 internal constant KERNEL_CORE_APP_ID = 0x3b4bf6bf3ad5000ecf0f989d5befde585c6860fea3e574a4fab4c49d1c177d9c; bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = 0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a; bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = 0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1; + bytes32 internal constant KERNEL_DEFAULT_KILL_SWITCH_APP_ID = 0x498cc0d31a6b7824a695121dd7e3f77a2b8f1108ed5a3367ec52d064799ee9cc; } diff --git a/contracts/kill-switch/IIssuesRegistry.sol b/contracts/kill-switch/IIssuesRegistry.sol new file mode 100644 index 000000000..97c927a63 --- /dev/null +++ b/contracts/kill-switch/IIssuesRegistry.sol @@ -0,0 +1,14 @@ +pragma solidity 0.4.24; + + +contract IIssuesRegistry { + enum Severity { None, Low, Mid, High, Critical } + + event ChangeSeverity(address indexed implementation, Severity severity, address indexed sender); + + function setSeverityFor(address implementation, Severity severity) external; + + function hasSeverity(address implementation) public view returns (bool); + + function getSeverityFor(address implementation) public view returns (Severity); +} diff --git a/contracts/kill-switch/IKillSwitch.sol b/contracts/kill-switch/IKillSwitch.sol new file mode 100644 index 000000000..cacec5ff8 --- /dev/null +++ b/contracts/kill-switch/IKillSwitch.sol @@ -0,0 +1,8 @@ +pragma solidity 0.4.24; + +import "./IIssuesRegistry.sol"; + + +contract IKillSwitch { + function shouldDenyCallingApp(bytes32 _appId, address _base, address _proxy) external view returns (bool); +} diff --git a/contracts/kill-switch/IssuesRegistry.sol b/contracts/kill-switch/IssuesRegistry.sol new file mode 100644 index 000000000..892d80d47 --- /dev/null +++ b/contracts/kill-switch/IssuesRegistry.sol @@ -0,0 +1,31 @@ +pragma solidity 0.4.24; + +import "../apps/AragonApp.sol"; +import "./IIssuesRegistry.sol"; + + +contract IssuesRegistry is IIssuesRegistry, AragonApp { + bytes32 constant public CHANGE_SEVERITY_ROLE = keccak256("CHANGE_SEVERITY_ROLE"); + + mapping (address => Severity) internal issuesSeverity; + + function initialize() external onlyInit { + initialized(); + } + + function setSeverityFor(address implementation, Severity severity) + external + authP(CHANGE_SEVERITY_ROLE, arr(implementation, uint256(issuesSeverity[implementation]), uint256(severity))) + { + issuesSeverity[implementation] = severity; + emit ChangeSeverity(implementation, severity, msg.sender); + } + + function hasSeverity(address implementation) public view isInitialized returns (bool) { + return issuesSeverity[implementation] != Severity.None; + } + + function getSeverityFor(address implementation) public view isInitialized returns (Severity) { + return issuesSeverity[implementation]; + } +} diff --git a/contracts/kill-switch/KillSwitch.sol b/contracts/kill-switch/KillSwitch.sol new file mode 100644 index 000000000..6e6a3b21a --- /dev/null +++ b/contracts/kill-switch/KillSwitch.sol @@ -0,0 +1,142 @@ +pragma solidity 0.4.24; + +import "./IKillSwitch.sol"; +import "./IIssuesRegistry.sol"; +import "../apps/AragonApp.sol"; +import "../common/IsContract.sol"; + + +contract KillSwitch is IKillSwitch, IsContract, AragonApp { + /* + * Hardcoded constants to save gas + * bytes32 constant public CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE = keccak256("CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE"); + * bytes32 constant public CHANGE_WHITELISTED_INSTANCES_ROLE = keccak256("CHANGE_WHITELISTED_INSTANCES_ROLE"); + * bytes32 constant public CHANGE_BLACKLISTED_BASE_IMPLS_ROLE = keccak256("CHANGE_BLACKLISTED_BASE_IMPLS_ROLE"); + * bytes32 constant public CHANGE_ISSUES_REGISTRY_ROLE = keccak256("CHANGE_ISSUES_REGISTRY_ROLE"); + * bytes32 constant public CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE = keccak256("CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE"); + */ + bytes32 constant public CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE = 0xdc8509ec9a919d33309806f4c91c281bcd27100bf2f895bcf78c5b42a0c39517; + bytes32 constant public CHANGE_WHITELISTED_INSTANCES_ROLE = 0x015a45e5f33fcae59ca7bd74eb36669dbf842f279d59011ea683d2867d05464a; + bytes32 constant public CHANGE_BLACKLISTED_BASE_IMPLS_ROLE = 0x05c71f33783f36a1b1a40c12d7308ff84c475600d0a4ff736122d42d72eafd4c; + bytes32 constant public CHANGE_ISSUES_REGISTRY_ROLE = 0x05b8a6bf0cdb51438256b73559daacd20b321e9c934d472dddb8f6cf12e6e048; + bytes32 constant public CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE = 0x1aec2a88cc5515dccebf91f7653b986b872c1cea4b784dc2eb5d285a6ccb2998; + + string constant private ERROR_ISSUES_REGISTRY_NOT_CONTRACT = "KS_ISSUES_REGISTRY_NOT_CONTRACT"; + + struct IssuesSettings { + IIssuesRegistry issuesRegistry; + IIssuesRegistry.Severity highestAllowedSeverity; + } + + IIssuesRegistry public defaultIssuesRegistry; + mapping (address => bool) internal whitelistedInstances; + mapping (address => bool) internal blacklistedBaseImplementations; + mapping (bytes32 => IssuesSettings) internal appsIssuesSettings; + + event ChangeDefaultIssuesRegistry(address indexed issuesRegistry); + event ChangeWhitelistedInstance(address indexed instance, bool whitelisted); + event ChangeBlacklistedBaseImplementation(address indexed base, bool blacklisted); + event ChangeIssuesRegistry(bytes32 indexed appId, address issuesRegistry); + event ChangeHighestAllowedSeverity(bytes32 indexed appId, IIssuesRegistry.Severity severity); + + function initialize(IIssuesRegistry _defaultIssuesRegistry) external onlyInit { + initialized(); + _setDefaultIssuesRegistry(_defaultIssuesRegistry); + } + + function setDefaultIssuesRegistry(IIssuesRegistry _defaultIssuesRegistry) + external + auth(CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE) + { + _setDefaultIssuesRegistry(_defaultIssuesRegistry); + } + + function setWhitelistedInstance(address _instance, bool _allowed) + external + authP(CHANGE_WHITELISTED_INSTANCES_ROLE, arr(_instance, whitelistedInstances[_instance], _allowed)) + { + whitelistedInstances[_instance] = _allowed; + emit ChangeWhitelistedInstance(_instance, _allowed); + } + + function setBlacklistedBaseImplementation(address _base, bool _denied) + external + authP(CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, arr(_base, blacklistedBaseImplementations[_base], _denied)) + { + blacklistedBaseImplementations[_base] = _denied; + emit ChangeBlacklistedBaseImplementation(_base, _denied); + } + + function setIssuesRegistry(bytes32 _appId, IIssuesRegistry _issuesRegistry) + external + authP(CHANGE_ISSUES_REGISTRY_ROLE, arr(_appId, address(appsIssuesSettings[_appId].issuesRegistry), address(_issuesRegistry))) + { + require(isContract(_issuesRegistry), ERROR_ISSUES_REGISTRY_NOT_CONTRACT); + appsIssuesSettings[_appId].issuesRegistry = _issuesRegistry; + emit ChangeIssuesRegistry(_appId, address(_issuesRegistry)); + } + + function setHighestAllowedSeverity(bytes32 _appId, IIssuesRegistry.Severity _severity) + external + authP(CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, arr(_appId, uint256(appsIssuesSettings[_appId].highestAllowedSeverity), uint256(_severity))) + { + appsIssuesSettings[_appId].highestAllowedSeverity = _severity; + emit ChangeHighestAllowedSeverity(_appId, _severity); + } + + /** + * @dev Note that we are not checking if the appId, base address and instance address are valid and if they correspond + * to each other in order to reduce extra calls. However, since this is only a query method, wrong input + * can only result in invalid output. Internally, this method is used from the Kernel to stop calls if needed, + * and we have several tests to make sure its usage is working as expected. + */ + function shouldDenyCallingApp(bytes32 _appId, address _base, address _instance) external view returns (bool) { + // if the instance is the kill switch itself, then allow given call + if (_instance == address(this)) { + return false; + } + + // if the instance is whitelisted, then allow given call + if (isInstanceWhitelisted(_instance)) { + return false; + } + + // if the base implementation is blacklisted, then deny given call + if (isBaseImplementationBlacklisted(_base)) { + return true; + } + + // Check if there is a severity issue reported in the corresponding issue registry. If there is actually a + // severity issue, check if it has exceeded the highest allowed severity level or not. + return hasExceededAllowedSeverity(_appId, _base); + } + + function isInstanceWhitelisted(address _instance) public view returns (bool) { + return whitelistedInstances[_instance]; + } + + function isBaseImplementationBlacklisted(address _base) public view returns (bool) { + return blacklistedBaseImplementations[_base]; + } + + function hasExceededAllowedSeverity(bytes32 _appId, address _base) public view returns (bool) { + IIssuesRegistry.Severity severityFound = getIssuesRegistry(_appId).getSeverityFor(_base); + IIssuesRegistry.Severity highestAllowedSeverity = getHighestAllowedSeverity(_appId); + return highestAllowedSeverity < severityFound; + } + + function getIssuesRegistry(bytes32 _appId) public view returns (IIssuesRegistry) { + IIssuesRegistry foundRegistry = appsIssuesSettings[_appId].issuesRegistry; + return foundRegistry == IIssuesRegistry(0) ? defaultIssuesRegistry : foundRegistry; + } + + function getHighestAllowedSeverity(bytes32 _appId) public view returns (IIssuesRegistry.Severity) { + return appsIssuesSettings[_appId].highestAllowedSeverity; + } + + function _setDefaultIssuesRegistry(IIssuesRegistry _defaultIssuesRegistry) internal { + require(isContract(_defaultIssuesRegistry), ERROR_ISSUES_REGISTRY_NOT_CONTRACT); + defaultIssuesRegistry = _defaultIssuesRegistry; + emit ChangeDefaultIssuesRegistry(address(_defaultIssuesRegistry)); + } +} diff --git a/contracts/test/mocks/common/KeccakConstants.sol b/contracts/test/mocks/common/KeccakConstants.sol index 6cc9a5bb5..6ccd3adc6 100644 --- a/contracts/test/mocks/common/KeccakConstants.sol +++ b/contracts/test/mocks/common/KeccakConstants.sol @@ -22,11 +22,19 @@ contract KeccakConstants { bytes32 public constant KERNEL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("kernel"))); bytes32 public constant DEFAULT_ACL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("acl"))); bytes32 public constant DEFAULT_VAULT_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("vault"))); + bytes32 public constant DEFAULT_KILL_SWITCH_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("kill-switch"))); // ACL bytes32 public constant CREATE_PERMISSIONS_ROLE = keccak256(abi.encodePacked("CREATE_PERMISSIONS_ROLE")); bytes32 public constant EMPTY_PARAM_HASH = keccak256(abi.encodePacked(uint256(0))); + // KillSwitch + bytes32 constant public CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE = keccak256("CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE"); + bytes32 constant public CHANGE_WHITELISTED_INSTANCES_ROLE = keccak256("CHANGE_WHITELISTED_INSTANCES_ROLE"); + bytes32 constant public CHANGE_BLACKLISTED_BASE_IMPLS_ROLE = keccak256("CHANGE_BLACKLISTED_BASE_IMPLS_ROLE"); + bytes32 constant public CHANGE_ISSUES_REGISTRY_ROLE = keccak256("CHANGE_ISSUES_REGISTRY_ROLE"); + bytes32 constant public CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE = keccak256("CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE"); + // APMRegistry bytes32 public constant CREATE_REPO_ROLE = keccak256(abi.encodePacked("CREATE_REPO_ROLE")); diff --git a/contracts/test/mocks/kernel/KernelConstantsMock.sol b/contracts/test/mocks/kernel/KernelConstantsMock.sol index 47c634848..a7b263606 100644 --- a/contracts/test/mocks/kernel/KernelConstantsMock.sol +++ b/contracts/test/mocks/kernel/KernelConstantsMock.sol @@ -12,4 +12,5 @@ contract KernelConstantsMock is Kernel { function getKernelAppId() external pure returns (bytes32) { return KERNEL_CORE_APP_ID; } function getDefaultACLAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_ACL_APP_ID; } function getDefaultVaultAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_VAULT_APP_ID; } + function getDefaultKillSwitchAppId() external pure returns (bytes32) { return KERNEL_DEFAULT_KILL_SWITCH_APP_ID; } } diff --git a/contracts/test/mocks/kernel/KernelOverloadMock.sol b/contracts/test/mocks/kernel/KernelOverloadMock.sol index 66d87ec2b..57ba0014b 100644 --- a/contracts/test/mocks/kernel/KernelOverloadMock.sol +++ b/contracts/test/mocks/kernel/KernelOverloadMock.sol @@ -11,40 +11,26 @@ import "../../../lib/misc/ERCProxy.sol"; * NOTE: awkwardly, by default we have access to the full version of `newAppInstance()` but only the * minimized version for `newPinnedAppInstance()` */ -contract KernelOverloadMock { - Kernel public kernel; +contract KernelOverloadMock is Kernel { + constructor(bool _shouldPetrify) Kernel(_shouldPetrify) public {} - event NewAppProxy(address proxy); + // Overriding function to bypass Truffle's overloading issues + function newAppInstanceWithoutPayload(bytes32 _appId, address _appBase) public returns (ERCProxy) { + return super.newAppInstance(_appId, _appBase); + } - constructor(Kernel _kernel) public { - kernel = _kernel; + // Overriding function to bypass Truffle's overloading issues + function newAppInstanceWithPayload(bytes32 _appId, address _appBase, bytes _initializePayload, bool _setDefault) public returns (ERCProxy) { + return super.newAppInstance(_appId, _appBase, _initializePayload, _setDefault); } - /* - function newAppInstance(bytes32 _appId, address _appBase) - public - auth(APP_MANAGER_ROLE, arr(KERNEL_APP_BASES_NAMESPACE, _appId)) - returns (ERCProxy appProxy) - */ - function newAppInstance(bytes32 _appId, address _appBase) - public - returns (ERCProxy appProxy) - { - appProxy = kernel.newAppInstance(_appId, _appBase); - emit NewAppProxy(appProxy); + // Overriding function to bypass Truffle's overloading issues + function newPinnedAppInstanceWithoutPayload(bytes32 _appId, address _appBase) public returns (ERCProxy) { + return super.newPinnedAppInstance(_appId, _appBase); } - /* - function newPinnedAppInstance(bytes32 _appId, address _appBase, bytes _initializePayload, bool _setDefault) - public - auth(APP_MANAGER_ROLE, arr(KERNEL_APP_BASES_NAMESPACE, _appId)) - returns (ERCProxy appProxy) - */ - function newPinnedAppInstance(bytes32 _appId, address _appBase, bytes _initializePayload, bool _setDefault) - public - returns (ERCProxy appProxy) - { - appProxy = kernel.newPinnedAppInstance(_appId, _appBase, _initializePayload, _setDefault); - emit NewAppProxy(appProxy); + // Overriding function to bypass Truffle's overloading issues + function newPinnedAppInstanceWithPayload(bytes32 _appId, address _appBase, bytes _initializePayload, bool _setDefault) public returns (ERCProxy) { + return super.newPinnedAppInstance(_appId, _appBase, _initializePayload, _setDefault); } } diff --git a/contracts/test/mocks/kill-switch/KernelWithNonCompliantKillSwitchMock.sol b/contracts/test/mocks/kill-switch/KernelWithNonCompliantKillSwitchMock.sol new file mode 100644 index 000000000..997928138 --- /dev/null +++ b/contracts/test/mocks/kill-switch/KernelWithNonCompliantKillSwitchMock.sol @@ -0,0 +1,18 @@ +pragma solidity 0.4.24; + +import "../../../kernel/Kernel.sol"; + + +/** + * @title KernelWithNonCompliantKillSwitchMock + * @dev This mock mimics a situation where the kernel returns an unexpected result for a kill-switch check + */ +contract KernelWithNonCompliantKillSwitchMock is Kernel { + constructor() Kernel(true) public {} + + function isAppDisabled(bytes32 _appId, address _instance) public view returns (bool) { + assembly { + return(0, 0x40) // returning 2 words instead of one + } + } +} diff --git a/contracts/test/mocks/kill-switch/KernelWithoutKillSwitchMock.sol b/contracts/test/mocks/kill-switch/KernelWithoutKillSwitchMock.sol new file mode 100644 index 000000000..a5bc9d3f8 --- /dev/null +++ b/contracts/test/mocks/kill-switch/KernelWithoutKillSwitchMock.sol @@ -0,0 +1,22 @@ +pragma solidity 0.4.24; + +import "../../../kernel/Kernel.sol"; + + +/** + * @title KernelWithoutKillSwitchMock + * @dev This mock mimics an already deployed Kernel version that does not have a kill-switch integrated + */ +contract KernelWithoutKillSwitchMock is Kernel { + string private constant ERROR_METHOD_NOT_FOUND = "KERNEL_METHOD_NOT_FOUND"; + + constructor() Kernel(true) public {} + + function killSwitch() public view returns (IKillSwitch) { + revert(ERROR_METHOD_NOT_FOUND); + } + + function isAppDisabled(bytes32 _appId, address _instance) public view returns (bool) { + revert(ERROR_METHOD_NOT_FOUND); + } +} diff --git a/contracts/test/mocks/kill-switch/KillSwitchedAppMock.sol b/contracts/test/mocks/kill-switch/KillSwitchedAppMock.sol new file mode 100644 index 000000000..aa0c4c075 --- /dev/null +++ b/contracts/test/mocks/kill-switch/KillSwitchedAppMock.sol @@ -0,0 +1,50 @@ +pragma solidity 0.4.24; + +import "../../../apps/AragonApp.sol"; + + +contract KillSwitchedAppMock is AragonApp { + bytes32 public constant WRITER_ROLE = keccak256("WRITER_ROLE"); + string private constant ERROR_AUTH_FAILED = "APP_AUTH_FAILED"; + + address public owner; + uint256 internal data; + + modifier oldAuth(bytes32 _role) { + require(_oldCanPerform(msg.sender, _role, new uint256[](0)), ERROR_AUTH_FAILED); + _; + } + + function initialize(address _owner) public onlyInit { + initialized(); + data = 42; + owner = _owner; + } + + function read() public view returns (uint256) { + return data; + } + + function write(uint256 _data) public auth(WRITER_ROLE) { + data = _data; + } + + function writeWithoutKillSwitch(uint256 _data) oldAuth(WRITER_ROLE) public { + data = _data; + } + + function reset() public auth(WRITER_ROLE) { + data = 0; + } + + function _oldCanPerform(address _sender, bytes32 _role, uint256[] _params) private view returns (bool) { + if (!hasInitialized()) { + return false; + } + IKernel _kernel = kernel(); + if (address(_kernel) == address(0)) { + return false; + } + return _kernel.hasPermission(_sender, address(this), _role, ConversionHelpers.dangerouslyCastUintArrayToBytes(_params)); + } +} diff --git a/contracts/test/mocks/kill-switch/RevertingKillSwitchMock.sol b/contracts/test/mocks/kill-switch/RevertingKillSwitchMock.sol new file mode 100644 index 000000000..c2f5f003c --- /dev/null +++ b/contracts/test/mocks/kill-switch/RevertingKillSwitchMock.sol @@ -0,0 +1,12 @@ +pragma solidity 0.4.24; + +import "../../../kill-switch/KillSwitch.sol"; + + +contract RevertingKillSwitchMock is KillSwitch { + string private constant ERROR_MESSAGE = "KILL_SWITCH_REVERTED!"; + + function shouldDenyCallingApp(bytes32 _appId, address _base, address _instance) external view returns (bool) { + revert(ERROR_MESSAGE); + } +} diff --git a/scripts/deploy-daofactory.js b/scripts/deploy-daofactory.js index f3bfa9829..06c018ae8 100644 --- a/scripts/deploy-daofactory.js +++ b/scripts/deploy-daofactory.js @@ -6,6 +6,7 @@ const ZERO_ADDR = '0x0000000000000000000000000000000000000000' const defaultKernelBase = process.env.KERNEL_BASE const defaultAclBaseAddress = process.env.ACL_BASE +const defaultKillSwitchBaseAddress = process.env.KILL_SWITCH_BASE module.exports = async ( truffleExecCallback, @@ -23,7 +24,7 @@ module.exports = async ( const ACL = artifacts.require('ACL') const Kernel = artifacts.require('Kernel') - + const KillSwitch = artifacts.require('KillSwitch') const DAOFactory = artifacts.require('DAOFactory') let kernelBase @@ -44,6 +45,15 @@ module.exports = async ( await logDeploy(aclBase, { verbose }) } + let killSwitchBase + if (defaultKillSwitchBaseAddress) { + killSwitchBase = KillSwitch.at(defaultKillSwitchBaseAddress) + log(`Skipping deploying new KillSwitch base, using provided address: ${defaultKillSwitchBaseAddress}`) + } else { + killSwitchBase = await KillSwitch.new() + await logDeploy(killSwitchBase, { verbose }) + } + let evmScriptRegistryFactory if (withEvmScriptRegistryFactory) { const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') @@ -53,6 +63,7 @@ module.exports = async ( const daoFactory = await DAOFactory.new( kernelBase.address, aclBase.address, + killSwitchBase.address, evmScriptRegistryFactory ? evmScriptRegistryFactory.address : ZERO_ADDR ) @@ -64,6 +75,7 @@ module.exports = async ( } else { return { aclBase, + killSwitchBase, daoFactory, evmScriptRegistryFactory, kernelBase, diff --git a/test/contracts/apm/apm_registry.js b/test/contracts/apm/apm_registry.js index 79e84af5a..c29cc50fb 100644 --- a/test/contracts/apm/apm_registry.js +++ b/test/contracts/apm/apm_registry.js @@ -9,6 +9,7 @@ const PublicResolver = artifacts.require('PublicResolver') const Kernel = artifacts.require('Kernel') const ACL = artifacts.require('ACL') +const KillSwitch = artifacts.require('KillSwitch') const DAOFactory = artifacts.require('DAOFactory') const APMRegistry = artifacts.require('APMRegistry') @@ -35,7 +36,8 @@ contract('APMRegistry', ([ensOwner, apmOwner, repoDev, notOwner, someone]) => { const kernelBase = await Kernel.new(true) // petrify immediately const aclBase = await ACL.new() - daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR) + const killSwitchBase = await KillSwitch.new() + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, ZERO_ADDR) }) beforeEach(async () => { diff --git a/test/contracts/common/keccak_constants.js b/test/contracts/common/keccak_constants.js index 972bb9b03..8d460495b 100644 --- a/test/contracts/common/keccak_constants.js +++ b/test/contracts/common/keccak_constants.js @@ -27,6 +27,7 @@ contract('Constants', () => { assert.equal(await kernelConstants.getKernelAppId(), await keccakConstants.KERNEL_APP_ID(), "kernel app id doesn't match") assert.equal(await kernelConstants.getDefaultACLAppId(), await keccakConstants.DEFAULT_ACL_APP_ID(), "default ACL id doesn't match") assert.equal(await kernelConstants.getDefaultVaultAppId(), await keccakConstants.DEFAULT_VAULT_APP_ID(), "default vault id doesn't match") + assert.equal(await kernelConstants.getDefaultKillSwitchAppId(), await keccakConstants.DEFAULT_KILL_SWITCH_APP_ID(), "default kill switch id doesn't match") assert.equal(await kernelConstants.getKernelCoreNamespace(), await keccakConstants.KERNEL_CORE_NAMESPACE(), "core namespace doesn't match") assert.equal(await kernelConstants.getKernelAppBasesNamespace(), await keccakConstants.KERNEL_APP_BASES_NAMESPACE(), "base namespace doesn't match") assert.equal(await kernelConstants.getKernelAppAddrNamespace(), await keccakConstants.KERNEL_APP_ADDR_NAMESPACE(), "app namespace doesn't match") @@ -35,6 +36,7 @@ contract('Constants', () => { assert.equal(await kernel.APP_MANAGER_ROLE(), await keccakConstants.APP_MANAGER_ROLE(), "app manager role doesn't match") assert.equal(await kernel.KERNEL_APP_ID(), await keccakConstants.KERNEL_APP_ID(), "app id doesn't match") assert.equal(await kernel.DEFAULT_ACL_APP_ID(), await keccakConstants.DEFAULT_ACL_APP_ID(), "default acl id doesn't match") + assert.equal(await kernel.DEFAULT_KILL_SWITCH_APP_ID(), await keccakConstants.DEFAULT_KILL_SWITCH_APP_ID(), "default kill switch id doesn't match") assert.equal(await kernel.CORE_NAMESPACE(), await keccakConstants.KERNEL_CORE_NAMESPACE(), "core namespace doesn't match") assert.equal(await kernel.APP_BASES_NAMESPACE(), await keccakConstants.KERNEL_APP_BASES_NAMESPACE(), "base namespace doesn't match") assert.equal(await kernel.APP_ADDR_NAMESPACE(), await keccakConstants.KERNEL_APP_ADDR_NAMESPACE(), "app namespace doesn't match") @@ -121,4 +123,14 @@ contract('Constants', () => { // redefined the storage position correctly in the mock. assert.equal(await reentrancyGuardMock.getReentrancyMutexPosition(), await keccakConstants.reentrancyGuardPosition(), "reentrancyGuardPosition doesn't match") }) + + it('checks KillSwitch constants', async () => { + const killSwitch = await getContract('KillSwitch').new() + + assert.equal(await killSwitch.CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE(), await keccakConstants.CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE()) + assert.equal(await killSwitch.CHANGE_WHITELISTED_INSTANCES_ROLE(), await keccakConstants.CHANGE_WHITELISTED_INSTANCES_ROLE()) + assert.equal(await killSwitch.CHANGE_BLACKLISTED_BASE_IMPLS_ROLE(), await keccakConstants.CHANGE_BLACKLISTED_BASE_IMPLS_ROLE()) + assert.equal(await killSwitch.CHANGE_ISSUES_REGISTRY_ROLE(), await keccakConstants.CHANGE_ISSUES_REGISTRY_ROLE()) + assert.equal(await killSwitch.CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE(), await keccakConstants.CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE()) + }) }) diff --git a/test/contracts/ens/ens_subdomains.js b/test/contracts/ens/ens_subdomains.js index d34b4640d..1f0d77251 100644 --- a/test/contracts/ens/ens_subdomains.js +++ b/test/contracts/ens/ens_subdomains.js @@ -9,6 +9,7 @@ const ENSFactory = artifacts.require('ENSFactory') const Repo = artifacts.require('Repo') const ACL = artifacts.require('ACL') const Kernel = artifacts.require('Kernel') +const KillSwitch = artifacts.require('KillSwitch') const DAOFactory = artifacts.require('DAOFactory') const APMRegistry = artifacts.require('APMRegistry') const APMRegistryFactory = artifacts.require('APMRegistryFactory') @@ -34,7 +35,8 @@ contract('ENSSubdomainRegistrar', ([_, apmOwner, notOwner]) => { const kernelBase = await Kernel.new(true) // petrify immediately const aclBase = await ACL.new() - daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR) + const killSwitchBase = await KillSwitch.new() + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, ZERO_ADDR) APP_BASES_NAMESPACE = await kernelBase.APP_BASES_NAMESPACE() }) diff --git a/test/contracts/evmscript/evm_script.js b/test/contracts/evmscript/evm_script.js index 3b36ae593..03ec44e93 100644 --- a/test/contracts/evmscript/evm_script.js +++ b/test/contracts/evmscript/evm_script.js @@ -191,7 +191,7 @@ contract('EVM Script', ([_, boss]) => { }) context('> ScriptRunner', () => { - let scriptRunnerApp + let scriptRunnerAppBase, scriptRunnerApp before(async () => { scriptRunnerAppBase = await AppStubScriptRunner.new() diff --git a/test/contracts/factory/dao_factory.js b/test/contracts/factory/dao_factory.js index c2603e3db..30d93f227 100644 --- a/test/contracts/factory/dao_factory.js +++ b/test/contracts/factory/dao_factory.js @@ -1,26 +1,32 @@ +const { assertRevert } = require('../../helpers/assertThrow') +const { getEventArgument } = require('../../helpers/events') +const { assertEvent, assertAmountOfEvents } = require('../../helpers/assertEvent')(web3) + const DAOFactory = artifacts.require('DAOFactory') const ACL = artifacts.require('ACL') const Kernel = artifacts.require('Kernel') +const KillSwitch = artifacts.require('KillSwitch') +const IssuesRegistry = artifacts.require('IssuesRegistry') const EVMScriptRegistry = artifacts.require('EVMScriptRegistry') const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') const EVMScriptRegistryConstants = artifacts.require('EVMScriptRegistryConstantsMock') -const ZERO_ADDRES = '0x0000000000000000000000000000000000000000' - -const getEventArgument = (receipt, event, arg) => receipt.logs.filter(l => l.event === event)[0].args[arg] +const ZERO_ADDR = '0x0000000000000000000000000000000000000000' contract('DAO Factory', ([_, root]) => { let daoFactory, dao, acl, receipt let CORE_NAMESPACE, APP_ADDR_NAMESPACE, APP_BASES_NAMESPACE let APP_MANAGER_ROLE, CREATE_PERMISSIONS_ROLE, REGISTRY_ADD_EXECUTOR_ROLE - let ACL_APP_ID, KERNEL_APP_ID, EVM_SCRIPT_REGISTRY_APP_ID - let kernelBase, aclBase, scriptsRegistryFactory, scriptsRegistryBase, scriptsRegistryConstants + let ACL_APP_ID, KERNEL_APP_ID, KILL_SWITCH_APP_ID, EVM_SCRIPT_REGISTRY_APP_ID + let kernelBase, aclBase, killSwitchBase, issuesRegistry, scriptsRegistryFactory, scriptsRegistryBase, scriptsRegistryConstants before('deploy base implementations', async () => { kernelBase = await Kernel.new(true) // petrify immediately aclBase = await ACL.new() + killSwitchBase = await KillSwitch.new() + issuesRegistry = await IssuesRegistry.new() scriptsRegistryFactory = await EVMScriptRegistryFactory.new() scriptsRegistryConstants = await EVMScriptRegistryConstants.new() scriptsRegistryBase = EVMScriptRegistry.at(await scriptsRegistryFactory.baseReg()) @@ -29,6 +35,7 @@ contract('DAO Factory', ([_, root]) => { before('load roles and constants', async () => { ACL_APP_ID = await kernelBase.DEFAULT_ACL_APP_ID() KERNEL_APP_ID = await kernelBase.KERNEL_APP_ID() + KILL_SWITCH_APP_ID = await kernelBase.DEFAULT_KILL_SWITCH_APP_ID() EVM_SCRIPT_REGISTRY_APP_ID = await scriptsRegistryConstants.getEVMScriptRegistryAppId() CORE_NAMESPACE = await kernelBase.CORE_NAMESPACE() @@ -56,7 +63,7 @@ contract('DAO Factory', ([_, root]) => { }) it('does not create or grant app manager to the root address of the DAO', async () => { - assert.equal(await acl.getPermissionManager(dao.address, APP_MANAGER_ROLE), ZERO_ADDRES) + assert.equal(await acl.getPermissionManager(dao.address, APP_MANAGER_ROLE), ZERO_ADDR) assert.isFalse(await acl.hasPermission(root, dao.address, APP_MANAGER_ROLE)) assert.isFalse(await acl.hasPermission(daoFactory.address, dao.address, APP_MANAGER_ROLE)) }) @@ -64,7 +71,7 @@ contract('DAO Factory', ([_, root]) => { const itDoesCreateAnEVMScriptsRegistry = () => { it('deploys an EVM script registry with a script executor', async () => { - const scriptsRegistry = EVMScriptRegistry.at(getEventArgument(receipt, 'DeployEVMScriptRegistry', 'reg')) + const scriptsRegistry = EVMScriptRegistry.at(getEventArgument(receipt, 'DeployEVMScriptRegistry', 'registry')) assert(await scriptsRegistry.hasInitialized(), 'EVM scripts registry should be initialized') assert.equal(await dao.getApp(APP_ADDR_NAMESPACE, EVM_SCRIPT_REGISTRY_APP_ID), scriptsRegistry.address) @@ -73,7 +80,7 @@ contract('DAO Factory', ([_, root]) => { const [executor] = await scriptsRegistry.executors(1) assert.equal(executor, await scriptsRegistryFactory.baseCallScript()) - assert.equal(await acl.getPermissionManager(scriptsRegistry.address, REGISTRY_ADD_EXECUTOR_ROLE), ZERO_ADDRES) + assert.equal(await acl.getPermissionManager(scriptsRegistry.address, REGISTRY_ADD_EXECUTOR_ROLE), ZERO_ADDR) assert.isFalse(await acl.hasPermission(root, scriptsRegistry.address, REGISTRY_ADD_EXECUTOR_ROLE)) assert.isFalse(await acl.hasPermission(scriptsRegistryFactory.address, scriptsRegistry.address, REGISTRY_ADD_EXECUTOR_ROLE)) }) @@ -81,15 +88,47 @@ contract('DAO Factory', ([_, root]) => { const itDoesNotCreateAnEVMScriptsRegistry = () => { it('does not deploy an EVM script registry with a script executor', async () => { - assert.equal(await dao.getApp(APP_ADDR_NAMESPACE, EVM_SCRIPT_REGISTRY_APP_ID), ZERO_ADDRES) - assert.equal(await dao.getApp(APP_BASES_NAMESPACE, EVM_SCRIPT_REGISTRY_APP_ID), ZERO_ADDRES) + assert.equal(await dao.getApp(APP_ADDR_NAMESPACE, EVM_SCRIPT_REGISTRY_APP_ID), ZERO_ADDR) + assert.equal(await dao.getApp(APP_BASES_NAMESPACE, EVM_SCRIPT_REGISTRY_APP_ID), ZERO_ADDR) + }) + } + + const itDoesCreateAKillSwitch = () => { + it('does install a kill switch instance', async () => { + const killSwitch = KillSwitch.at(await dao.killSwitch()) + + assert.equal(await dao.getApp(APP_ADDR_NAMESPACE, KILL_SWITCH_APP_ID), killSwitch.address) + assert.equal(await dao.getApp(APP_BASES_NAMESPACE, KILL_SWITCH_APP_ID), killSwitchBase.address) + }) + + it('whitelists the kernel, acl and kill switch instances by default', async () => { + const killSwitch = KillSwitch.at(await dao.killSwitch()) + + assert(await killSwitch.isInstanceWhitelisted(dao.address), 'Kernel instance should be whitelisted') + assert(await killSwitch.isInstanceWhitelisted(acl.address), 'ACL instance should be whitelisted') + assert(await killSwitch.isInstanceWhitelisted(killSwitch.address), 'KillSwitch instance should be whitelisted') + }) + + it('emits an event', async () => { + assertAmountOfEvents(receipt, 'DeployKillSwitch', 1) + + const killSwitch = KillSwitch.at(await dao.killSwitch()) + assertEvent(receipt, 'DeployKillSwitch', { killSwitch: killSwitch.address }) + }) + } + + const itDoesNotCreateAKillSwitch = () => { + it('does not have a kill switch installed', async () => { + assert.equal(await dao.killSwitch(), ZERO_ADDR) + assert.equal(await dao.getApp(APP_ADDR_NAMESPACE, KILL_SWITCH_APP_ID), ZERO_ADDR) + assert.equal(await dao.getApp(APP_BASES_NAMESPACE, KILL_SWITCH_APP_ID), ZERO_ADDR) }) } describe('newDAO', () => { context('when it was created with an EVM scripts registry factory', () => { before('create factory with an EVM scripts registry factory', async () => { - daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, scriptsRegistryFactory.address) + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR, scriptsRegistryFactory.address) }) before('create a DAO', async () => { @@ -100,11 +139,12 @@ contract('DAO Factory', ([_, root]) => { itCreatesADao() itDoesCreateAnEVMScriptsRegistry() + itDoesNotCreateAKillSwitch() }) context('when it was created without an EVM scripts registry factory', () => { before('create factory without an EVM scripts registry factory', async () => { - daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDRES) + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR, ZERO_ADDR) }) before('create a DAO', async () => { @@ -115,6 +155,65 @@ contract('DAO Factory', ([_, root]) => { itCreatesADao() itDoesNotCreateAnEVMScriptsRegistry() + itDoesNotCreateAKillSwitch() + }) + }) + + describe('newDAOWithKillSwitch', () => { + context('when it was created with a base kill switch', () => { + context('when it was created with an EVM scripts registry factory', () => { + before('create factory with an EVM scripts registry factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, scriptsRegistryFactory.address) + }) + + before('create a DAO', async () => { + receipt = await daoFactory.newDAOWithKillSwitch(root, issuesRegistry.address) + dao = Kernel.at(getEventArgument(receipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + }) + + itCreatesADao() + itDoesCreateAnEVMScriptsRegistry() + itDoesCreateAKillSwitch() + }) + + context('when it was created without an EVM scripts registry factory', () => { + before('create factory without an EVM scripts registry factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, ZERO_ADDR) + }) + + before('create a DAO', async () => { + receipt = await daoFactory.newDAOWithKillSwitch(root, issuesRegistry.address) + dao = Kernel.at(getEventArgument(receipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + }) + + itCreatesADao() + itDoesNotCreateAnEVMScriptsRegistry() + itDoesCreateAKillSwitch() + }) + }) + + context('when it was created without a base kill switch', () => { + context('when it was created with an EVM scripts registry factory', () => { + before('create factory with an EVM scripts registry factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR, scriptsRegistryFactory.address) + }) + + it('reverts', async () => { + await assertRevert(daoFactory.newDAOWithKillSwitch(root, issuesRegistry.address), 'DF_MISSING_BASE_KILL_SWITCH') + }) + }) + + context('when it was created without an EVM scripts registry factory', () => { + before('create factory without an EVM scripts registry factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, ZERO_ADDR, ZERO_ADDR) + }) + + it('reverts', async () => { + await assertRevert(daoFactory.newDAOWithKillSwitch(root, issuesRegistry.address), 'DF_MISSING_BASE_KILL_SWITCH') + }) + }) }) }) }) diff --git a/test/contracts/factory/evm_script_factory.js b/test/contracts/factory/evm_script_factory.js index d75dc612d..767dea7dc 100644 --- a/test/contracts/factory/evm_script_factory.js +++ b/test/contracts/factory/evm_script_factory.js @@ -4,6 +4,7 @@ const { getEventArgument, getNewProxyAddress } = require('../../helpers/events') const Kernel = artifacts.require('Kernel') const ACL = artifacts.require('ACL') +const KillSwitch = artifacts.require('KillSwitch') const EVMScriptRegistry = artifacts.require('EVMScriptRegistry') const DAOFactory = artifacts.require('DAOFactory') const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') @@ -27,9 +28,10 @@ contract('EVM Script Factory', ([permissionsRoot]) => { before(async () => { const kernelBase = await Kernel.new(true) // petrify immediately const aclBase = await ACL.new() + const killSwitchBase = await KillSwitch.new() regFact = await EVMScriptRegistryFactory.new() - daoFact = await DAOFactory.new(kernelBase.address, aclBase.address, regFact.address) + daoFact = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, regFact.address) callsScriptBase = await regFact.baseCallScript() evmScriptRegBase = EVMScriptRegistry.at(await regFact.baseReg()) const evmScriptRegConstants = await EVMScriptRegistryConstantsMock.new() @@ -47,7 +49,7 @@ contract('EVM Script Factory', ([permissionsRoot]) => { beforeEach(async () => { const receipt = await daoFact.newDAO(permissionsRoot) dao = Kernel.at(getEventArgument(receipt, 'DeployDAO', 'dao')) - evmScriptReg = EVMScriptRegistry.at(getEventArgument(receipt, 'DeployEVMScriptRegistry', 'reg')) + evmScriptReg = EVMScriptRegistry.at(getEventArgument(receipt, 'DeployEVMScriptRegistry', 'registry')) acl = ACL.at(await dao.acl()) }) diff --git a/test/contracts/kernel/kernel_apps.js b/test/contracts/kernel/kernel_apps.js index 49fea658b..1931cbc6b 100644 --- a/test/contracts/kernel/kernel_apps.js +++ b/test/contracts/kernel/kernel_apps.js @@ -5,7 +5,7 @@ const { getNewProxyAddress } = require('../../helpers/events') const { assertAmountOfEvents } = require('../../helpers/assertEvent')(web3) const ACL = artifacts.require('ACL') -const Kernel = artifacts.require('Kernel') +const Kernel = artifacts.require('KernelOverloadMock') const KernelProxy = artifacts.require('KernelProxy') const AppProxyUpgradeable = artifacts.require('AppProxyUpgradeable') const AppProxyPinned = artifacts.require('AppProxyPinned') @@ -14,7 +14,6 @@ const AppProxyPinned = artifacts.require('AppProxyPinned') const AppStub = artifacts.require('AppStub') const AppStub2 = artifacts.require('AppStub2') const ERCProxyMock = artifacts.require('ERCProxyMock') -const KernelOverloadMock = artifacts.require('KernelOverloadMock') const APP_ID = hash('stub.aragonpm.test') const EMPTY_BYTES = '0x' @@ -45,7 +44,7 @@ contract('Kernel apps', ([permissionsRoot]) => { // Test both the Kernel itself and the KernelProxy to make sure their behaviours are the same for (const kernelType of ['Kernel', 'KernelProxy']) { context(`> ${kernelType}`, () => { - let acl, kernel, kernelBase, app, appProxy + let acl, kernel, kernelBase before(async () => { if (kernelType === 'KernelProxy') { @@ -77,26 +76,14 @@ contract('Kernel apps', ([permissionsRoot]) => { await assertRevert(kernel.setApp(APP_BASES_NAMESPACE, APP_ID, '0x1234')) }) - const newAppProxyMapping = { - 'AppProxy': 'newAppInstance', - 'AppProxyPinned': 'newPinnedAppInstance', - } - for (const appProxyType of Object.keys(newAppProxyMapping)) { - // NOTE: we have to do really hacky workarounds here due to truffle not supporting - // function overloads. - // Especially awful is how we only get the full version of `newAppInstance()` but - // not `newPinnedAppInstance()`, forcing us to apply the KernelOverloadMock on - // different proxy instances - let kernelOverload - const newInstanceFn = newAppProxyMapping[appProxyType] - + for (const appProxyType of ['AppProxy', 'AppProxyPinned']) { const onlyAppProxy = onlyIf(() => appProxyType === 'AppProxy') const onlyAppProxyPinned = onlyIf(() => appProxyType === 'AppProxyPinned') context(`> new ${appProxyType} instances`, () => { onlyAppProxy(() => it('creates a new upgradeable app proxy instance', async () => { - const receipt = await kernel.newAppInstance(APP_ID, appBase1.address, EMPTY_BYTES, false) + const receipt = await kernel.newAppInstanceWithPayload(APP_ID, appBase1.address, EMPTY_BYTES, false) const appProxy = AppProxyUpgradeable.at(getNewProxyAddress(receipt)) assert.equal(await appProxy.kernel(), kernel.address, "new appProxy instance's kernel should be set to the originating kernel") @@ -108,7 +95,7 @@ contract('Kernel apps', ([permissionsRoot]) => { onlyAppProxyPinned(() => it('creates a new non upgradeable app proxy instance', async () => { - const receipt = await kernel.newPinnedAppInstance(APP_ID, appBase1.address) + const receipt = await kernel.newPinnedAppInstanceWithoutPayload(APP_ID, appBase1.address) const appProxy = AppProxyPinned.at(getNewProxyAddress(receipt)) assert.equal(await appProxy.kernel(), kernel.address, "new appProxy instance's kernel should be set to the originating kernel") @@ -119,31 +106,23 @@ contract('Kernel apps', ([permissionsRoot]) => { ) context('> full new app instance overload', async () => { - beforeEach(async () => { - if (appProxyType === 'AppProxy') { - // No need to apply the overload - kernelOverload = kernel - } else if (appProxyType === 'AppProxyPinned') { - kernelOverload = await KernelOverloadMock.new(kernel.address) - await acl.grantPermission(kernelOverload.address, kernel.address, APP_MANAGER_ROLE) - } - }) + const newInstanceFn = appProxyType === 'AppProxy' ? 'newAppInstanceWithPayload' : 'newPinnedAppInstanceWithPayload' it('sets the app base when not previously registered', async() => { assert.equal(ZERO_ADDR, await kernel.getApp(APP_BASES_NAMESPACE, APP_ID)) - await kernelOverload[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, false) + await kernel[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, false) assert.equal(appBase1.address, await kernel.getApp(APP_BASES_NAMESPACE, APP_ID)) }) it("doesn't set the app base when already set", async() => { await kernel.setApp(APP_BASES_NAMESPACE, APP_ID, appBase1.address) - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, false) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, false) assertAmountOfEvents(receipt, 'SetApp', 0) }) it("also sets the default app", async () => { - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, true) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address, EMPTY_BYTES, true) const appProxyAddr = getNewProxyAddress(receipt) // Check that both the app base and default app are set @@ -158,7 +137,7 @@ contract('Kernel apps', ([permissionsRoot]) => { const initData = appBase1.initialize.request().params[0].data // Make sure app was initialized - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address, initData, false) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address, initData, false) const appProxyAddr = getNewProxyAddress(receipt) assert.isTrue(await AppStub.at(appProxyAddr).hasInitialized(), 'App should have been initialized') @@ -168,7 +147,7 @@ contract('Kernel apps', ([permissionsRoot]) => { }) it("fails if the app base is not given", async() => { - await assertRevert(kernelOverload[newInstanceFn](APP_ID, ZERO_ADDR, EMPTY_BYTES, false)) + await assertRevert(kernel[newInstanceFn](APP_ID, ZERO_ADDR, EMPTY_BYTES, false)) }) it('fails if the given app base is different than the existing one', async() => { @@ -177,36 +156,28 @@ contract('Kernel apps', ([permissionsRoot]) => { assert.notEqual(existingBase, differentBase, 'appBase1 and appBase2 should have different addresses') await kernel.setApp(APP_BASES_NAMESPACE, APP_ID, existingBase) - await assertRevert(kernelOverload[newInstanceFn](APP_ID, differentBase, EMPTY_BYTES, false)) + await assertRevert(kernel[newInstanceFn](APP_ID, differentBase, EMPTY_BYTES, false)) }) }) context('> minimized new app instance overload', async () => { - beforeEach(async () => { - if (appProxyType === 'AppProxy') { - kernelOverload = await KernelOverloadMock.new(kernel.address) - await acl.grantPermission(kernelOverload.address, kernel.address, APP_MANAGER_ROLE) - } else if (appProxyType === 'AppProxyPinned') { - // No need to apply the overload - kernelOverload = kernel - } - }) + const newInstanceFn = appProxyType === 'AppProxy' ? 'newAppInstanceWithoutPayload' : 'newPinnedAppInstanceWithoutPayload' it('sets the app base when not previously registered', async() => { assert.equal(ZERO_ADDR, await kernel.getApp(APP_BASES_NAMESPACE, APP_ID)) - await kernelOverload[newInstanceFn](APP_ID, appBase1.address) + await kernel[newInstanceFn](APP_ID, appBase1.address) assert.equal(appBase1.address, await kernel.getApp(APP_BASES_NAMESPACE, APP_ID)) }) it("doesn't set the app base when already set", async() => { await kernel.setApp(APP_BASES_NAMESPACE, APP_ID, appBase1.address) - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address) assertAmountOfEvents(receipt, 'SetApp', 0) }) it("does not set the default app", async () => { - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address) const appProxyAddr = getNewProxyAddress(receipt) // Check that only the app base is set @@ -218,7 +189,7 @@ contract('Kernel apps', ([permissionsRoot]) => { }) it("does not allow initializing proxy", async () => { - const receipt = await kernelOverload[newInstanceFn](APP_ID, appBase1.address) + const receipt = await kernel[newInstanceFn](APP_ID, appBase1.address) const appProxyAddr = getNewProxyAddress(receipt) // Make sure app was not initialized @@ -230,7 +201,7 @@ contract('Kernel apps', ([permissionsRoot]) => { }) it("fails if the app base is not given", async() => { - await assertRevert(kernelOverload[newInstanceFn](APP_ID, ZERO_ADDR)) + await assertRevert(kernel[newInstanceFn](APP_ID, ZERO_ADDR)) }) it('fails if the given app base is different than the existing one', async() => { @@ -239,7 +210,7 @@ contract('Kernel apps', ([permissionsRoot]) => { assert.notEqual(existingBase, differentBase, 'appBase1 and appBase2 should have different addresses') await kernel.setApp(APP_BASES_NAMESPACE, APP_ID, existingBase) - await assertRevert(kernelOverload[newInstanceFn](APP_ID, differentBase)) + await assertRevert(kernel[newInstanceFn](APP_ID, differentBase)) }) }) }) diff --git a/test/contracts/kernel/kernel_lifecycle.js b/test/contracts/kernel/kernel_lifecycle.js index 832a05295..5425b38bc 100644 --- a/test/contracts/kernel/kernel_lifecycle.js +++ b/test/contracts/kernel/kernel_lifecycle.js @@ -23,7 +23,7 @@ contract('Kernel lifecycle', ([root, someone]) => { assert.isFalse(await kernel.hasPermission(someone, kernel.address, APP_MANAGER_ROLE, EMPTY_BYTES)) await assertRevert(kernel.newAppInstance(APP_ID, appBase.address, EMPTY_BYTES, false)) - await assertRevert(kernel.newPinnedAppInstance(APP_ID, appBase.address)) + await assertRevert(kernel.newPinnedAppInstance(APP_ID, appBase.address, EMPTY_BYTES, false)) await assertRevert(kernel.setApp(APP_BASES_NAMESPACE, APP_ID, appBase.address)) await assertRevert(kernel.setRecoveryVaultAppId(VAULT_ID)) } diff --git a/test/contracts/kill-switch/enums.js b/test/contracts/kill-switch/enums.js new file mode 100644 index 000000000..26374b223 --- /dev/null +++ b/test/contracts/kill-switch/enums.js @@ -0,0 +1,5 @@ +const SEVERITY = { NONE: 0, LOW: 1, MID: 2, HIGH: 3, CRITICAL: 4 } + +module.exports = { + SEVERITY +} diff --git a/test/contracts/kill-switch/issues_registry.js b/test/contracts/kill-switch/issues_registry.js new file mode 100644 index 000000000..1aefe5788 --- /dev/null +++ b/test/contracts/kill-switch/issues_registry.js @@ -0,0 +1,129 @@ +const { SEVERITY } = require('./enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { assertAmountOfEvents, assertEvent } = require('../../helpers/assertEvent')(web3) +const { getEventArgument, getNewProxyAddress } = require('../../helpers/events') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const KillSwitch = artifacts.require('KillSwitch') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('IssuesRegistry', ([_, root, implementation, owner, anyone]) => { + let kernelBase, aclBase, issuesRegistryBase, registryFactory, dao, acl, issuesRegistry, killSwitchBase + + before('deploy base implementations', async () => { + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + killSwitchBase = await KillSwitch.new() + registryFactory = await EVMScriptRegistryFactory.new() + issuesRegistryBase = await IssuesRegistry.new() + }) + + before('deploy DAO', async () => { + const daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, registryFactory.address) + const kernelReceipt = await daoFactory.newDAO(root) + dao = Kernel.at(getEventArgument(kernelReceipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + const APP_MANAGER_ROLE = await kernelBase.APP_MANAGER_ROLE() + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + }) + + beforeEach('create issues registry', async () => { + const receipt = await dao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getNewProxyAddress(receipt)) + await issuesRegistry.initialize() + const CHANGE_SEVERITY_ROLE = await issuesRegistryBase.CHANGE_SEVERITY_ROLE() + await acl.createPermission(owner, issuesRegistry.address, CHANGE_SEVERITY_ROLE, root, { from: root }) + }) + + describe('hasSeverity', () => { + context('when there was no severity set before', () => { + it('returns false', async () => { + assert.isFalse(await issuesRegistry.hasSeverity(implementation), 'did not expect severity for given implementation') + }) + }) + + context('when there was a severity already set', () => { + beforeEach('set medium severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from: owner }) + }) + + context('when the issue was real', () => { + it('returns true', async () => { + assert.isTrue(await issuesRegistry.hasSeverity(implementation), 'did not expect severity for given implementation') + }) + }) + + context('when the issue was a false positive', () => { + beforeEach('roll back severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.NONE, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await issuesRegistry.hasSeverity(implementation), 'did not expect severity for given implementation') + }) + }) + }) + }) + + describe('getSeverityFor', () => { + context('when there was no severity set before', () => { + it('returns none', async () => { + assert.equal(await issuesRegistry.getSeverityFor(implementation), SEVERITY.NONE, 'severity does not match') + }) + }) + + context('when there was a severity already set', () => { + beforeEach('set medium severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.MID, { from: owner }) + }) + + it('returns the severity already set', async () => { + assert.equal(await issuesRegistry.getSeverityFor(implementation), SEVERITY.MID, 'severity does not match') + }) + }) + }) + + describe('setSeverityFor', () => { + context('when the sender is the owner', () => { + const from = owner + + it('emits an event', async () => { + const receipt = await issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from }) + + assertAmountOfEvents(receipt, 'ChangeSeverity') + assertEvent(receipt, 'ChangeSeverity', { implementation, severity: SEVERITY.LOW, sender: owner }) + }) + + context('when there was no severity set before', () => { + it('sets the severity for the given implementation', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.MID, { from }) + + assert.equal(await issuesRegistry.getSeverityFor(implementation), SEVERITY.MID, 'severity does not match') + }) + }) + + context('when there was a severity already set', () => { + beforeEach('set medium severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.MID, { from }) + }) + + it('changes the severity for the given implementation', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from }) + + assert.equal(await issuesRegistry.getSeverityFor(implementation), SEVERITY.LOW, 'severity does not match') + }) + }) + }) + + context('when the sender is not the owner', () => { + const from = anyone + + it('reverts', async () => { + await assertRevert(issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from })) + }) + }) + }) +}) diff --git a/test/contracts/kill-switch/kill_switch.js b/test/contracts/kill-switch/kill_switch.js new file mode 100644 index 000000000..ffc5dc8e6 --- /dev/null +++ b/test/contracts/kill-switch/kill_switch.js @@ -0,0 +1,560 @@ +const { SEVERITY } = require('./enums') +const { skipCoverage } = require('../../helpers/coverage') +const { assertRevert } = require('../../helpers/assertThrow') +const { getNewProxyAddress } = require('../../helpers/events') +const { assertEvent, assertAmountOfEvents } = require('../../helpers/assertEvent')(web3) + +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const KillSwitch = artifacts.require('KillSwitch') +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KillSwitchedApp = artifacts.require('KillSwitchedAppMock') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const SAMPLE_APP_ID = '0x1236000000000000000000000000000000000000000000000000000000000000' + +contract('KillSwitch', ([_, root, owner, securityPartner, anyone]) => { + let kernelBase, aclBase, appBase, killSwitchBase, issuesRegistryBase + let dao, acl, app, killSwitch, defaultIssuesRegistry, specificIssuesRegistry + let CORE_NAMESPACE, KERNEL_APP_ID, KILL_SWITCH_APP_ID + let APP_MANAGER_ROLE, CHANGE_SEVERITY_ROLE, CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE, CHANGE_ISSUES_REGISTRY_ROLE, CHANGE_WHITELISTED_INSTANCES_ROLE, CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, WRITER_ROLE + + before('deploy base implementations', async () => { + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + killSwitchBase = await KillSwitch.new() + issuesRegistryBase = await IssuesRegistry.new() + appBase = await KillSwitchedApp.new() + }) + + before('load constants', async () => { + CORE_NAMESPACE = await kernelBase.CORE_NAMESPACE() + KERNEL_APP_ID = await kernelBase.KERNEL_APP_ID() + KILL_SWITCH_APP_ID = await kernelBase.DEFAULT_KILL_SWITCH_APP_ID() + }) + + before('load roles', async () => { + WRITER_ROLE = await appBase.WRITER_ROLE() + APP_MANAGER_ROLE = await kernelBase.APP_MANAGER_ROLE() + CHANGE_SEVERITY_ROLE = await issuesRegistryBase.CHANGE_SEVERITY_ROLE() + CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE = await killSwitchBase.CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE() + CHANGE_ISSUES_REGISTRY_ROLE = await killSwitchBase.CHANGE_ISSUES_REGISTRY_ROLE() + CHANGE_WHITELISTED_INSTANCES_ROLE = await killSwitchBase.CHANGE_WHITELISTED_INSTANCES_ROLE() + CHANGE_BLACKLISTED_BASE_IMPLS_ROLE = await killSwitchBase.CHANGE_BLACKLISTED_BASE_IMPLS_ROLE() + CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE = await killSwitchBase.CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE() + }) + + beforeEach('create issues registries', async () => { + const issuesRegistryDAO = await Kernel.new(false) + await issuesRegistryDAO.initialize(aclBase.address, root) + const issuesRegistryACL = ACL.at(await issuesRegistryDAO.acl()) + await issuesRegistryACL.createPermission(root, issuesRegistryDAO.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = issuesRegistryBase.contract.initialize.getData() + + const defaultRegistryReceipt = await issuesRegistryDAO.newAppInstance('0x1234', issuesRegistryBase.address, initializeData, false, { from: root }) + defaultIssuesRegistry = IssuesRegistry.at(getNewProxyAddress(defaultRegistryReceipt)) + await issuesRegistryACL.createPermission(securityPartner, defaultIssuesRegistry.address, CHANGE_SEVERITY_ROLE, root, { from: root }) + + const specificRegistryReceipt = await issuesRegistryDAO.newAppInstance('0x1234', issuesRegistryBase.address, initializeData, false, { from: root }) + specificIssuesRegistry = IssuesRegistry.at(getNewProxyAddress(specificRegistryReceipt)) + await issuesRegistryACL.createPermission(securityPartner, specificIssuesRegistry.address, CHANGE_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('deploy DAO with a kill switch', async () => { + dao = await Kernel.new(false) + await dao.initialize(aclBase.address, root) + acl = ACL.at(await dao.acl()) + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = killSwitchBase.contract.initialize.getData(defaultIssuesRegistry.address) + const receipt = await dao.newAppInstance(KILL_SWITCH_APP_ID, killSwitchBase.address, initializeData, true, { from: root }) + killSwitch = KillSwitch.at(getNewProxyAddress(receipt)) + + await acl.createPermission(owner, killSwitch.address, CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_WHITELISTED_INSTANCES_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create kill switched app', async () => { + const initializeData = appBase.contract.initialize.getData(owner) + const receipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(receipt)) + + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + }) + + describe('isInstanceWhitelisted', function () { + context('when there was no instance whitelisted value set yet', function () { + it('returns false', async () => { + assert.isFalse(await killSwitch.isInstanceWhitelisted(app.address)) + }) + }) + + context('when there was an whitelisted value already set', function () { + context('when it is whitelisted', function () { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from: owner }) + }) + + it('returns true', async () => { + assert(await killSwitch.isInstanceWhitelisted(app.address)) + }) + }) + + context('when it is not whitelisted', function () { + beforeEach('do not whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.isInstanceWhitelisted(app.address)) + }) + }) + }) + }) + + describe('setWhitelistedInstance', function () { + context('when the sender is authorized', function () { + const from = owner + + context('when there was no instance whitelisted yet', function () { + it('sets a new whitelisted value', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from }) + + assert(await killSwitch.isInstanceWhitelisted(app.address)) + }) + + it('emits an event', async () => { + const receipt = await killSwitch.setWhitelistedInstance(app.address, true, { from }) + + assertAmountOfEvents(receipt, 'ChangeWhitelistedInstance') + assertEvent(receipt, 'ChangeWhitelistedInstance', { whitelisted: true, instance: app.address }) + }) + }) + + context('when there was a instance already whitelisted', function () { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from }) + }) + + it('changes the whitelisted value', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from }) + + assert.isFalse(await killSwitch.isInstanceWhitelisted(app.address)) + }) + }) + }) + + context('when the sender is not authorized', function () { + const from = anyone + + it('reverts', async () => { + await assertRevert(killSwitch.setWhitelistedInstance(app.address, true, { from })) + }) + }) + }) + + describe('isBaseImplementationBlacklisted', function () { + context('when there was no blacklisted value set yet', function () { + it('returns false', async () => { + assert.isFalse(await killSwitch.isBaseImplementationBlacklisted(appBase.address)) + }) + }) + + context('when there was a blacklisted value already set', function () { + context('when it is blacklisted', function () { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + it('returns true', async () => { + assert.isTrue(await killSwitch.isBaseImplementationBlacklisted(appBase.address)) + }) + }) + + context('when it is not blacklisted', function () { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.isBaseImplementationBlacklisted(appBase.address)) + }) + }) + }) + }) + + describe('setBlacklistedBaseImplementation', function () { + context('when the sender is authorized', function () { + const from = owner + + context('when there was no base implementation blacklisted yet', function () { + it('sets a new blacklisted value', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from }) + + assert(await killSwitch.isBaseImplementationBlacklisted(appBase.address)) + }) + + it('emits an event', async () => { + const receipt = await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from }) + + assertAmountOfEvents(receipt, 'ChangeBlacklistedBaseImplementation') + assertEvent(receipt, 'ChangeBlacklistedBaseImplementation', { base: appBase.address, blacklisted: true }) + }) + }) + + context('when there was a base implementation already blacklisted', function () { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from }) + }) + + it('changes the blacklisted value', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from }) + + assert.isFalse(await killSwitch.isBaseImplementationBlacklisted(appBase.address)) + }) + }) + }) + + context('when the sender is not authorized', function () { + const from = anyone + + it('reverts', async () => { + await assertRevert(killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from }), 'APP_AUTH_FAILED') + }) + }) + }) + + describe('getIssuesRegistry', function () { + context('when there was no specific issues registry set', () => { + it('returns the default registry', async () => { + assert.equal(await killSwitch.getIssuesRegistry(SAMPLE_APP_ID), defaultIssuesRegistry.address) + }) + }) + + context('when there is a specific issues registry set', () => { + beforeEach('set specific issues registry', async () => { + await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from: owner }) + }) + + it('returns the default registry', async () => { + assert.equal(await killSwitch.getIssuesRegistry(SAMPLE_APP_ID), specificIssuesRegistry.address) + }) + }) + }) + + describe('setIssuesRegistry', function () { + context('when the sender is authorized', function () { + const from = owner + + context('when the given address is not a contract', () => { + it('reverts', async () => { + await assertRevert(killSwitch.setIssuesRegistry(SAMPLE_APP_ID, ZERO_ADDRESS, { from })) + }) + }) + + context('when the given address is a contract', () => { + context('when there was no specific issues registry set yet', function () { + it('sets the given implementation', async () => { + await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from }) + + assert.equal(await killSwitch.getIssuesRegistry(SAMPLE_APP_ID), specificIssuesRegistry.address) + }) + + it('emits an event', async () => { + const receipt = await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from }) + + assertAmountOfEvents(receipt, 'ChangeIssuesRegistry') + assertEvent(receipt, 'ChangeIssuesRegistry', { appId: SAMPLE_APP_ID, issuesRegistry: specificIssuesRegistry.address }) + }) + }) + + context('when there was a specific issues registry set', function () { + beforeEach('set specific issues registry', async () => { + await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from }) + }) + + it('changes the issues registry', async () => { + await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, defaultIssuesRegistry.address, { from }) + + assert.equal(await killSwitch.getIssuesRegistry(SAMPLE_APP_ID), defaultIssuesRegistry.address) + }) + }) + }) + }) + + context('when the sender is not authorized', function () { + const from = anyone + + it('reverts', async () => { + await assertRevert(killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from })) + }) + }) + }) + + describe('setDefaultIssuesRegistry', function () { + context('when the sender is authorized', function () { + const from = owner + + context('when the given address is not a contract', () => { + it('reverts', async () => { + await assertRevert(killSwitch.setDefaultIssuesRegistry(ZERO_ADDRESS, { from })) + }) + }) + + context('when the given address is a contract', () => { + context('when there was no specific issues registry set yet', function () { + it('sets the given implementation', async () => { + await killSwitch.setDefaultIssuesRegistry(specificIssuesRegistry.address, { from }) + + assert.equal(await killSwitch.defaultIssuesRegistry(), specificIssuesRegistry.address) + }) + + it('emits an event', async () => { + const receipt = await killSwitch.setDefaultIssuesRegistry(specificIssuesRegistry.address, { from }) + + assertAmountOfEvents(receipt, 'ChangeDefaultIssuesRegistry') + assertEvent(receipt, 'ChangeDefaultIssuesRegistry', { issuesRegistry: specificIssuesRegistry.address }) + }) + }) + + context('when there was a specific issues registry set', function () { + beforeEach('set specific issues registry', async () => { + await killSwitch.setDefaultIssuesRegistry(specificIssuesRegistry.address, { from }) + assert.equal(await killSwitch.defaultIssuesRegistry(), specificIssuesRegistry.address) + }) + + it('changes the issues registry', async () => { + await killSwitch.setDefaultIssuesRegistry(defaultIssuesRegistry.address, { from }) + + assert.equal(await killSwitch.defaultIssuesRegistry(), defaultIssuesRegistry.address) + }) + }) + }) + }) + + context('when the sender is not authorized', function () { + const from = anyone + + it('reverts', async () => { + await assertRevert(killSwitch.setDefaultIssuesRegistry(specificIssuesRegistry.address, { from })) + }) + }) + }) + + describe('hasExceededAllowedSeverity', function () { + context('when there is no bug registered', () => { + context('when there is no highest allowed severity set for the contract being called', () => { + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + }) + + context('when there is a bug registered in the default issues registry', () => { + beforeEach('register a bug', async () => { + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when there is no specific issues registry set', () => { + context('when there is no highest allowed severity set for the contract being called', () => { + it('returns true', async () => { + assert.isTrue(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + context('when the highest allowed severity is under the reported bug severity', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + it('returns true', async () => { + assert.isTrue(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is equal to the reported bug severity', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is greater than the reported bug severity', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.CRITICAL, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + }) + }) + + context('when there is a specific issues registry set', () => { + beforeEach('set specific issues registry', async () => { + await killSwitch.setIssuesRegistry(SAMPLE_APP_ID, specificIssuesRegistry.address, { from: owner }) + }) + + context('when there is no bug registered in the specific issues registry', () => { + context('when there is no highest allowed severity set for the contract being called', () => { + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + context('when the highest allowed severity is under the reported bug severity of the default registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is equal to the reported bug severity of the default registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is greater than the reported bug severity of the default registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.CRITICAL, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + }) + }) + + context('when there is a bug registered in the specific issues registry higher than the one reported in the default registry', () => { + beforeEach('register a bug', async () => { + await specificIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.HIGH, { from: securityPartner }) + }) + + context('when there is no highest allowed severity set for the contract being called', () => { + it('returns true', async () => { + assert.isTrue(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + context('when the highest allowed severity is under the reported bug severity of the default registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + it('returns true', async () => { + assert.isTrue(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is equal to the reported bug severity of the default registry but lower than the reported bug of the specific registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + it('returns true', async () => { + assert.isTrue(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + + context('when the highest allowed severity is equal to the reported bug severity of the specific registry', () => { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.HIGH, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await killSwitch.hasExceededAllowedSeverity(SAMPLE_APP_ID, appBase.address)) + }) + }) + }) + }) + }) + }) + }) + + describe('setHighestAllowedSeverity', function () { + context('when the sender is authorized', function () { + const from = owner + + context('when there was no severity set', function () { + it('sets the highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.HIGH, { from }) + + assert.equal(await killSwitch.getHighestAllowedSeverity(SAMPLE_APP_ID), SEVERITY.HIGH) + }) + + it('emits an event', async () => { + const receipt = await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.HIGH, { from }) + + assertAmountOfEvents(receipt, 'ChangeHighestAllowedSeverity') + assertEvent(receipt, 'ChangeHighestAllowedSeverity', { appId: SAMPLE_APP_ID, severity: SEVERITY.HIGH }) + }) + }) + + context('when there was a previous severity set', function () { + beforeEach('set highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from }) + assert.equal(await killSwitch.getHighestAllowedSeverity(SAMPLE_APP_ID), SEVERITY.LOW) + }) + + it('changes the highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from }) + + assert.equal(await killSwitch.getHighestAllowedSeverity(SAMPLE_APP_ID), SEVERITY.MID) + }) + }) + }) + + context('when the sender is not authorized', function () { + const from = anyone + + it('reverts', async () => { + await assertRevert(killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from })) + }) + }) + }) + + describe('gas costs', () => { + beforeEach('set a highest allowed severity', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.LOW, { from: securityPartner }) + }) + + it('kill switch should overload ~27k of gas to a function', skipCoverage(async () => { + const { receipt: { cumulativeGasUsed: gasUsedWithKillSwitch } } = await app.write(10, { from: owner }) + const { receipt: { cumulativeGasUsed: gasUsedWithoutKillSwitch } } = await app.writeWithoutKillSwitch(10, { from: owner }) + + const killSwitchCost = gasUsedWithKillSwitch - gasUsedWithoutKillSwitch + assert.isAtMost(killSwitchCost, 27000, 'kill switch should have maximum overhead of ~27k of gas') + })) + }) +}) diff --git a/test/contracts/kill-switch/kill_switch_kernel.js b/test/contracts/kill-switch/kill_switch_kernel.js new file mode 100644 index 000000000..6445288a1 --- /dev/null +++ b/test/contracts/kill-switch/kill_switch_kernel.js @@ -0,0 +1,576 @@ +const { SEVERITY } = require('./enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { getNewProxyAddress, getEventArgument } = require('../../helpers/events') + +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const DAOFactory = artifacts.require('DAOFactory') +const KillSwitch = artifacts.require('KillSwitch') +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KillSwitchedApp = artifacts.require('KillSwitchedAppMock') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +const RevertingKillSwitchMock = artifacts.require('RevertingKillSwitchMock') +const KernelWithoutKillSwitchMock = artifacts.require('KernelWithoutKillSwitchMock') +const KernelWithNonCompliantKillSwitchMock = artifacts.require('KernelWithNonCompliantKillSwitchMock') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const SAMPLE_APP_ID = '0x1236000000000000000000000000000000000000000000000000000000000000' + +contract('KillSwitch Kernel', ([_, root, owner, securityPartner]) => { + let dao, acl, app, registryFactory + let kernelBase, aclBase, appBase, killSwitchBase, issuesRegistryBase, daoFactory + let kernelWithoutKillSwitchBase, kernelWithNonCompliantKillSwitchBase, failingKillSwitchBase + let CORE_NAMESPACE, KERNEL_APP_ID, APP_MANAGER_ROLE, CHANGE_SEVERITY_ROLE, CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE, CHANGE_ISSUES_REGISTRY_ROLE, CHANGE_WHITELISTED_INSTANCES_ROLE, CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, WRITER_ROLE + + before('deploy base implementations', async () => { + // real + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + registryFactory = await EVMScriptRegistryFactory.new() + killSwitchBase = await KillSwitch.new() + issuesRegistryBase = await IssuesRegistry.new() + + // mocks + appBase = await KillSwitchedApp.new() + failingKillSwitchBase = await RevertingKillSwitchMock.new() + kernelWithoutKillSwitchBase = await KernelWithoutKillSwitchMock.new() + kernelWithNonCompliantKillSwitchBase = await KernelWithNonCompliantKillSwitchMock.new() + }) + + before('load constants and roles', async () => { + WRITER_ROLE = await appBase.WRITER_ROLE() + CORE_NAMESPACE = await kernelBase.CORE_NAMESPACE() + KERNEL_APP_ID = await kernelBase.KERNEL_APP_ID() + APP_MANAGER_ROLE = await kernelBase.APP_MANAGER_ROLE() + CHANGE_SEVERITY_ROLE = await issuesRegistryBase.CHANGE_SEVERITY_ROLE() + CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE = await killSwitchBase.CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE() + CHANGE_ISSUES_REGISTRY_ROLE = await killSwitchBase.CHANGE_ISSUES_REGISTRY_ROLE() + CHANGE_WHITELISTED_INSTANCES_ROLE = await killSwitchBase.CHANGE_WHITELISTED_INSTANCES_ROLE() + CHANGE_BLACKLISTED_BASE_IMPLS_ROLE = await killSwitchBase.CHANGE_BLACKLISTED_BASE_IMPLS_ROLE() + CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE = await killSwitchBase.CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE() + }) + + context('when the kernel version does not support kill-switch logic', async () => { + before('create DAO factory', async () => { + daoFactory = await DAOFactory.new(kernelWithoutKillSwitchBase.address, aclBase.address, ZERO_ADDRESS, registryFactory.address) + }) + + beforeEach('deploy DAO without a kill switch and create kill-switched sample app', async () => { + const daoFactoryReceipt = await daoFactory.newDAO(root) + dao = Kernel.at(getEventArgument(daoFactoryReceipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = appBase.contract.initialize.getData(owner) + const appReceipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(appReceipt)) + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + }) + + it('executes the call', async () => { + await app.write(10, { from: owner }) + assert.equal(await app.read(), 10) + }) + }) + + context('when the kernel version does support kill-switch logic', async () => { + context('when the kernel was not initialized with a kill-switch', async () => { + before('create DAO factory using a kernel that supports kill-switch logic', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, registryFactory.address) + }) + + before('deploy DAO without a kill switch and create kill-switched sample app', async () => { + const daoFactoryReceipt = await daoFactory.newDAO(root) + dao = Kernel.at(getEventArgument(daoFactoryReceipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = appBase.contract.initialize.getData(owner) + const appReceipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(appReceipt)) + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + }) + + context('when the function being called is not tagged', () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + }) + + context('when the function being called is tagged', () => { + it('executes the call', async () => { + await app.write(10, { from: owner }) + assert.equal(await app.read(), 10) + }) + }) + }) + + context('when the kernel is initialized with a non-compliant kill-switch implementation', async () => { + before('create DAO factory', async () => { + daoFactory = await DAOFactory.new(kernelWithoutKillSwitchBase.address, aclBase.address, killSwitchBase.address, registryFactory.address) + }) + + before('deploy DAO with a kill switch and create kill-switched sample app', async () => { + const daoFactoryReceipt = await daoFactory.newDAOWithKillSwitch(root, issuesRegistryBase.address) + dao = Kernel.at(getEventArgument(daoFactoryReceipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = appBase.contract.initialize.getData(owner) + const receipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(receipt)) + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + + // upgrade kernel to non-compliant implementation + await dao.setApp(CORE_NAMESPACE, KERNEL_APP_ID, kernelWithNonCompliantKillSwitchBase.address, { from: root }) + }) + + context('when the function being called is not tagged', () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + }) + + context('when the function being called is tagged', () => { + it('does not execute the call', async () => { + await assertRevert(app.write(10, { from: owner }), 'APP_AUTH_FAILED') + }) + }) + }) + + context('when the kernel is initialized with a failing kill-switch implementation', async () => { + let killSwitch, defaultIssuesRegistry + + before('create DAO factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, failingKillSwitchBase.address, registryFactory.address) + }) + + before('create issues registry', async () => { + const daoReceipt = await daoFactory.newDAO(root) + const issuesRegistryDAO = Kernel.at(getEventArgument(daoReceipt, 'DeployDAO', 'dao')) + const issuesRegistryACL = ACL.at(await issuesRegistryDAO.acl()) + + await issuesRegistryACL.createPermission(root, issuesRegistryDAO.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = issuesRegistryBase.contract.initialize.getData() + const defaultRegistryReceipt = await issuesRegistryDAO.newAppInstance('0x1234', issuesRegistryBase.address, initializeData, false, { from: root }) + defaultIssuesRegistry = IssuesRegistry.at(getNewProxyAddress(defaultRegistryReceipt)) + await issuesRegistryACL.createPermission(securityPartner, defaultIssuesRegistry.address, CHANGE_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('deploy DAO with a kill switch', async () => { + const receipt = await daoFactory.newDAOWithKillSwitch(root, defaultIssuesRegistry.address) + dao = Kernel.at(getEventArgument(receipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + killSwitch = KillSwitch.at(await dao.killSwitch()) + + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_WHITELISTED_INSTANCES_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create kill switched app', async () => { + const initializeData = appBase.contract.initialize.getData(owner) + const receipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(receipt)) + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + }) + + const itExecutesTheCall = () => { + it('executes the call', async () => { + await app.write(10, { from: owner }) + assert.equal(await app.read(), 10) + }) + } + + context('when the function being called is not tagged', () => { + itExecutesTheCall() + }) + + context('when the function being called is tagged', () => { + const itAlwaysExecutesTheCall = () => { + context('when the instance being called is whitelisted', () => { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + // Note that whitelisting a single instance has higher precedence than blacklisting a base implementation + itExecutesTheCall() + }) + }) + + context('when the instance being called is not marked as whitelisted', () => { + beforeEach('do not whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + } + + context('when there is no bug registered', () => { + itAlwaysExecutesTheCall() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when there is no highest whitelisted severity set for the contract being called', () => { + itAlwaysExecutesTheCall() + }) + + context('when there is a highest whitelisted severity set for the contract being called', () => { + context('when the highest whitelisted severity is under the reported bug severity', () => { + beforeEach('set highest whitelisted severity below the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + itAlwaysExecutesTheCall() + }) + + context('when the highest whitelisted severity is equal to the reported bug severity', () => { + beforeEach('set highest whitelisted severity equal to the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + itAlwaysExecutesTheCall() + }) + + context('when the highest whitelisted severity is greater than the reported bug severity', () => { + beforeEach('set highest whitelisted severity above the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.CRITICAL, { from: owner }) + }) + + itAlwaysExecutesTheCall() + }) + }) + }) + }) + }) + + context('when the kernel is initialized with a safe kill-switch implementation', async () => { + let killSwitch, defaultIssuesRegistry + + before('create DAO factory', async () => { + daoFactory = await DAOFactory.new(kernelBase.address, aclBase.address, killSwitchBase.address, registryFactory.address) + }) + + beforeEach('create issues registries', async () => { + const daoReceipt = await daoFactory.newDAO(root) + const issuesRegistryDAO = Kernel.at(getEventArgument(daoReceipt, 'DeployDAO', 'dao')) + const issuesRegistryACL = ACL.at(await issuesRegistryDAO.acl()) + await issuesRegistryACL.createPermission(root, issuesRegistryDAO.address, APP_MANAGER_ROLE, root, { from: root }) + + const initializeData = issuesRegistryBase.contract.initialize.getData() + const defaultRegistryReceipt = await issuesRegistryDAO.newAppInstance('0x1234', issuesRegistryBase.address, initializeData, false, { from: root }) + defaultIssuesRegistry = IssuesRegistry.at(getNewProxyAddress(defaultRegistryReceipt)) + await issuesRegistryACL.createPermission(securityPartner, defaultIssuesRegistry.address, CHANGE_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('deploy DAO with a kill switch', async () => { + const receipt = await daoFactory.newDAOWithKillSwitch(root, defaultIssuesRegistry.address) + dao = Kernel.at(getEventArgument(receipt, 'DeployDAO', 'dao')) + acl = ACL.at(await dao.acl()) + killSwitch = KillSwitch.at(await dao.killSwitch()) + await acl.createPermission(root, dao.address, APP_MANAGER_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_DEFAULT_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_ISSUES_REGISTRY_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_WHITELISTED_INSTANCES_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_BLACKLISTED_BASE_IMPLS_ROLE, root, { from: root }) + await acl.createPermission(owner, killSwitch.address, CHANGE_HIGHEST_ALLOWED_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create kill switched app', async () => { + const initializeData = appBase.contract.initialize.getData(owner) + const receipt = await dao.newAppInstance(SAMPLE_APP_ID, appBase.address, initializeData, false, { from: root }) + app = KillSwitchedApp.at(getNewProxyAddress(receipt)) + await acl.createPermission(owner, app.address, WRITER_ROLE, root, { from: root }) + }) + + context('when the function being called is not tagged', () => { + + const itExecutesTheCallEvenWhenBaseImplementationIsBlacklisted = () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + } + + context('when the instance being called is whitelisted', () => { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + + context('when the instance being called is not marked as whitelisted', () => { + beforeEach('dot not whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + } + + context('when there is no bug registered', () => { + itExecutesTheCallEvenWhenBaseImplementationIsBlacklisted() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + itExecutesTheCallEvenWhenBaseImplementationIsBlacklisted() + }) + }) + + context('when the function being called is tagged', () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + await app.write(10, { from: owner }) + assert.equal(await app.read(), 10) + }) + } + + const itDoesNotExecuteTheCall = () => { + it('does not execute the call', async () => { + await assertRevert(app.write(10, { from: owner }), 'APP_AUTH_FAILED') + }) + } + + const itExecutesTheCallOnlyWhenWhitelisted = () => { + context('when the instance being called is whitelisted', () => { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + + context('when the instance being called is not marked as whitelisted', () => { + beforeEach('dot not whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + } + + const itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted = () => { + context('when the instance being called is whitelisted', () => { + beforeEach('whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, true, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + + context('when the instance being called is not marked as whitelisted', () => { + beforeEach('dot not whitelist instance', async () => { + await killSwitch.setWhitelistedInstance(app.address, false, { from: owner }) + }) + + context('when the base implementation is not blacklisted', () => { + beforeEach('do not blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, false, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the base implementation is blacklisted', () => { + beforeEach('blacklist base implementation', async () => { + await killSwitch.setBlacklistedBaseImplementation(appBase.address, true, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + } + + context('when there is no bug registered', () => { + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when the bug was real', () => { + context('when there is no highest allowed severity set for the contract being called', () => { + itExecutesTheCallOnlyWhenWhitelisted() + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + context('when the highest allowed severity is under the reported bug severity', () => { + beforeEach('set highest allowed severity below the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallOnlyWhenWhitelisted() + }) + + context('when the highest allowed severity is equal to the reported bug severity', () => { + beforeEach('set highest allowed severity equal to the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + + context('when the highest allowed severity is greater than the reported bug severity', () => { + beforeEach('set highest allowed severity above the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + }) + }) + + context('when the bug was a false positive', () => { + beforeEach('roll back reported bug', async () => { + await defaultIssuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + context('when there is no highest allowed severity set for the contract being called', () => { + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + + context('when there is a highest allowed severity set for the contract being called', () => { + context('when the highest allowed severity is under the reported bug severity', () => { + beforeEach('set highest allowed severity below the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + + context('when the highest allowed severity is equal to the reported bug severity', () => { + beforeEach('set highest allowed severity equal to the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + + context('when the highest allowed severity is greater than the reported bug severity', () => { + beforeEach('set highest allowed severity above the one reported', async () => { + await killSwitch.setHighestAllowedSeverity(SAMPLE_APP_ID, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallUnlessInstanceNotWhitelistedAndBaseBlacklisted() + }) + }) + }) + }) + }) + }) + }) +})