diff --git a/contracts/factory/DAOFactory.sol b/contracts/factory/DAOFactory.sol index a2eec709a..c88ac9771 100644 --- a/contracts/factory/DAOFactory.sol +++ b/contracts/factory/DAOFactory.sol @@ -3,6 +3,7 @@ pragma solidity 0.4.24; import "../kernel/IKernel.sol"; import "../kernel/Kernel.sol"; import "../kernel/KernelProxy.sol"; +import "../kill_switch/kernel/KernelKillSwitch.sol"; import "../acl/IACL.sol"; import "../acl/ACL.sol"; @@ -46,32 +47,54 @@ contract DAOFactory { dao.initialize(baseACL, _root); } else { dao.initialize(baseACL, this); + _setupNewDaoPermissions(_root, dao); + } - ACL acl = ACL(dao.acl()); - bytes32 permRole = acl.CREATE_PERMISSIONS_ROLE(); - bytes32 appManagerRole = dao.APP_MANAGER_ROLE(); + emit DeployDAO(address(dao)); + return dao; + } - acl.grantPermission(regFactory, acl, permRole); + /** + * @notice Create a new DAO with `_root` set as the initial admin and `_issuesRegistry` as the source of truth for kill-switch purpose + * @param _root Address that will be granted control to setup DAO permissions + * @param _issuesRegistry Address of the registry of issues that will be used in case of critical situations by the kernel kill switch + * @return Newly created DAO + */ + function newDAOWithKillSwitch(address _root, IssuesRegistry _issuesRegistry) public returns (KernelKillSwitch) { + KernelKillSwitch dao = KernelKillSwitch(new KernelProxy(baseKernel)); - acl.createPermission(regFactory, dao, appManagerRole, this); + if (address(regFactory) == address(0)) { + dao.initialize(_issuesRegistry, baseACL, _root); + } else { + dao.initialize(_issuesRegistry, baseACL, address(this)); + _setupNewDaoPermissions(_root, Kernel(dao)); + } - EVMScriptRegistry reg = regFactory.newEVMScriptRegistry(dao); - emit DeployEVMScriptRegistry(address(reg)); + emit DeployDAO(address(dao)); + return dao; + } - // Clean up permissions - // First, completely reset the APP_MANAGER_ROLE - acl.revokePermission(regFactory, dao, appManagerRole); - acl.removePermissionManager(dao, appManagerRole); + function _setupNewDaoPermissions(address _root, Kernel _dao) internal { + ACL acl = ACL(_dao.acl()); + bytes32 permRole = acl.CREATE_PERMISSIONS_ROLE(); + bytes32 appManagerRole = _dao.APP_MANAGER_ROLE(); - // 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); - } + acl.grantPermission(regFactory, acl, permRole); - emit DeployDAO(address(dao)); + acl.createPermission(regFactory, _dao, appManagerRole, this); - return dao; + EVMScriptRegistry reg = regFactory.newEVMScriptRegistry(_dao); + emit DeployEVMScriptRegistry(address(reg)); + + // Clean up permissions + // First, completely reset the APP_MANAGER_ROLE + acl.revokePermission(regFactory, _dao, appManagerRole); + acl.removePermissionManager(_dao, appManagerRole); + + // 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); } } diff --git a/contracts/kill_switch/app/AppBinaryKillSwitch.sol b/contracts/kill_switch/app/AppBinaryKillSwitch.sol new file mode 100644 index 000000000..b813755bd --- /dev/null +++ b/contracts/kill_switch/app/AppBinaryKillSwitch.sol @@ -0,0 +1,14 @@ +pragma solidity 0.4.24; + +import "./AppKillSwitch.sol"; +import "../base/BinaryKillSwitch.sol"; + + +contract AppBinaryKillSwitch is AppKillSwitch, BinaryKillSwitch { + function setContractAction(address _contract, ContractAction _action) + external + authP(SET_CONTRACT_ACTION_ROLE, arr(_baseApp(), msg.sender)) + { + _setContractAction(_contract, _action); + } +} diff --git a/contracts/kill_switch/app/AppKillSwitch.sol b/contracts/kill_switch/app/AppKillSwitch.sol new file mode 100644 index 000000000..a3e5569ff --- /dev/null +++ b/contracts/kill_switch/app/AppKillSwitch.sol @@ -0,0 +1,15 @@ +pragma solidity 0.4.24; + +import "../base/KillSwitch.sol"; + + +contract AppKillSwitch is AragonApp, KillSwitch { + function initialize(IssuesRegistry _issuesRegistry) public onlyInit { + initialized(); + _setIssuesRegistry(_issuesRegistry); + } + + function _baseApp() internal view returns (address) { + return kernel().getApp(KERNEL_APP_BASES_NAMESPACE, appId()); + } +} diff --git a/contracts/kill_switch/app/AppSeveritiesKillSwitch.sol b/contracts/kill_switch/app/AppSeveritiesKillSwitch.sol new file mode 100644 index 000000000..005732f9b --- /dev/null +++ b/contracts/kill_switch/app/AppSeveritiesKillSwitch.sol @@ -0,0 +1,21 @@ +pragma solidity 0.4.24; + +import "./AppKillSwitch.sol"; +import "../base/SeveritiesKillSwitch.sol"; + + +contract AppSeveritiesKillSwitch is AppKillSwitch, SeveritiesKillSwitch { + function setContractAction(address _contract, ContractAction _action) + external + authP(SET_CONTRACT_ACTION_ROLE, arr(_baseApp(), msg.sender)) + { + _setContractAction(_contract, _action); + } + + function setLowestAllowedSeverity(address _contract, IssuesRegistry.Severity _severity) + external + authP(SET_LOWEST_ALLOWED_SEVERITY_ROLE, arr(_baseApp(), msg.sender)) + { + _setLowestAllowedSeverity(_contract, _severity); + } +} diff --git a/contracts/kill_switch/app/KillSwitchedApp.sol b/contracts/kill_switch/app/KillSwitchedApp.sol new file mode 100644 index 000000000..d091433a9 --- /dev/null +++ b/contracts/kill_switch/app/KillSwitchedApp.sol @@ -0,0 +1,26 @@ +pragma solidity 0.4.24; + +import "./AppKillSwitch.sol"; +import "../../apps/AragonApp.sol"; + + +contract KillSwitchedApp is AragonApp { + string private constant ERROR_CONTRACT_CALL_NOT_ALLOWED = "APP_CONTRACT_CALL_NOT_ALLOWED"; + + AppKillSwitch internal appKillSwitch; + + modifier killSwitched { + bool _isCallAllowed = !appKillSwitch.shouldDenyCallingContract(_baseApp(), address(this), msg.sender, msg.data, msg.value); + require(_isCallAllowed, ERROR_CONTRACT_CALL_NOT_ALLOWED); + _; + } + + function initialize(AppKillSwitch _appKillSwitch) public onlyInit { + initialized(); + appKillSwitch = _appKillSwitch; + } + + function _baseApp() internal view returns (address) { + return kernel().getApp(KERNEL_APP_BASES_NAMESPACE, appId()); + } +} diff --git a/contracts/kill_switch/base/BinaryKillSwitch.sol b/contracts/kill_switch/base/BinaryKillSwitch.sol new file mode 100644 index 000000000..e5f507edd --- /dev/null +++ b/contracts/kill_switch/base/BinaryKillSwitch.sol @@ -0,0 +1,11 @@ +pragma solidity 0.4.24; + +import "./KillSwitch.sol"; +import "./IssuesRegistry.sol"; + + +contract BinaryKillSwitch is KillSwitch { + function isSeverityIgnored(address /*_contract*/, IssuesRegistry.Severity _severity) public view returns (bool) { + return _severity == IssuesRegistry.Severity.None; + } +} diff --git a/contracts/kill_switch/base/IssuesRegistry.sol b/contracts/kill_switch/base/IssuesRegistry.sol new file mode 100644 index 000000000..8a84359fb --- /dev/null +++ b/contracts/kill_switch/base/IssuesRegistry.sol @@ -0,0 +1,31 @@ +pragma solidity 0.4.24; + +import "../../apps/AragonApp.sol"; + + +contract IssuesRegistry is AragonApp { + bytes32 constant public SET_ENTRY_SEVERITY_ROLE = keccak256("SET_ENTRY_SEVERITY_ROLE"); + + enum Severity { None, Low, Mid, High, Critical } + + mapping (address => Severity) internal issuesSeverity; + + event SeveritySet(address indexed entry, Severity severity, address sender); + + function initialize() public onlyInit { + initialized(); + } + + function isSeverityFor(address entry) public view isInitialized returns (bool) { + return issuesSeverity[entry] != Severity.None; + } + + function getSeverityFor(address entry) public view isInitialized returns (Severity) { + return issuesSeverity[entry]; + } + + function setSeverityFor(address entry, Severity severity) public authP(SET_ENTRY_SEVERITY_ROLE, arr(entry, msg.sender)) { + issuesSeverity[entry] = severity; + emit SeveritySet(entry, severity, msg.sender); + } +} diff --git a/contracts/kill_switch/base/KillSwitch.sol b/contracts/kill_switch/base/KillSwitch.sol new file mode 100644 index 000000000..0228e3f86 --- /dev/null +++ b/contracts/kill_switch/base/KillSwitch.sol @@ -0,0 +1,84 @@ +pragma solidity 0.4.24; + +import "./IssuesRegistry.sol"; + + +contract KillSwitch { + bytes32 constant public SET_CONTRACT_ACTION_ROLE = keccak256("SET_CONTRACT_ACTION_ROLE"); + + enum ContractAction { Check, Ignore, Deny } + + IssuesRegistry public issuesRegistry; + mapping (address => ContractAction) internal contractActions; + + event IssuesRegistrySet(address issuesRegistry, address sender); + event ContractActionSet(address contractAddress, ContractAction action); + + function setContractAction(address _contract, ContractAction _action) external; + + function getContractAction(address _contract) public view returns (ContractAction) { + return contractActions[_contract]; + } + + function isSeverityIgnored(address _contract, IssuesRegistry.Severity _severity) public view returns (bool); + + function isContractIgnored(address _contract) public view returns (bool) { + return getContractAction(_contract) == ContractAction.Ignore; + } + + function isContractDenied(address _contract) public view returns (bool) { + return getContractAction(_contract) == ContractAction.Deny; + } + + function shouldDenyCallingContract(address _base, address _instance, address _sender, bytes _data, uint256 _value) public returns (bool) { + // if the call should not be evaluated, then allow given call + if (!_shouldEvaluateCall(_base, _instance, _sender, _data, _value)) { + return false; + } + + // if the call should be denied, then deny given call + if (isContractDenied(_base)) { + return true; + } + + // if the contract issues are ignored, then allow given call + if (isContractIgnored(_base)) { + return false; + } + + // if the issues registry has not been set, then allow given call + if (issuesRegistry == address(0)) { + return false; + } + + // if the contract severity found is ignored, then allow given call + IssuesRegistry.Severity _severityFound = issuesRegistry.getSeverityFor(_base); + if (isSeverityIgnored(_base, _severityFound)) { + return false; + } + + // if none of the conditions above were met, then deny given call + return true; + } + + /** + * @dev This function allows different kill-switch implementations to provide a custom logic to tell whether a + * certain call should be denied or not. This is important to ensure recoverability. For example, custom + * implementations could override this function to provide a decision based on the msg.sender, timestamp, + * block information, among many other options. + * @return Always true by default. + */ + function _shouldEvaluateCall(address, address, address, bytes, uint256) internal returns (bool) { + return true; + } + + function _setIssuesRegistry(IssuesRegistry _issuesRegistry) internal { + issuesRegistry = _issuesRegistry; + emit IssuesRegistrySet(_issuesRegistry, msg.sender); + } + + function _setContractAction(address _contract, ContractAction _action) internal { + contractActions[_contract] = _action; + emit ContractActionSet(_contract, _action); + } +} diff --git a/contracts/kill_switch/base/SeveritiesKillSwitch.sol b/contracts/kill_switch/base/SeveritiesKillSwitch.sol new file mode 100644 index 000000000..e38867944 --- /dev/null +++ b/contracts/kill_switch/base/SeveritiesKillSwitch.sol @@ -0,0 +1,25 @@ +pragma solidity 0.4.24; + +import "./KillSwitch.sol"; +import "./IssuesRegistry.sol"; + + +contract SeveritiesKillSwitch is KillSwitch { + bytes32 constant public SET_LOWEST_ALLOWED_SEVERITY_ROLE = keccak256("SET_LOWEST_ALLOWED_SEVERITY_ROLE"); + + mapping (address => IssuesRegistry.Severity) internal lowestAllowedSeverityByContract; + + event LowestAllowedSeveritySet(address indexed _contract, IssuesRegistry.Severity severity); + + function setLowestAllowedSeverity(address _contract, IssuesRegistry.Severity _severity) external; + + function isSeverityIgnored(address _contract, IssuesRegistry.Severity _severity) public view returns (bool) { + IssuesRegistry.Severity lowestAllowedSeverity = lowestAllowedSeverityByContract[_contract]; + return lowestAllowedSeverity >= _severity; + } + + function _setLowestAllowedSeverity(address _contract, IssuesRegistry.Severity _severity) internal { + lowestAllowedSeverityByContract[_contract] = _severity; + emit LowestAllowedSeveritySet(_contract, _severity); + } +} diff --git a/contracts/kill_switch/kernel/KernelBinaryKillSwitch.sol b/contracts/kill_switch/kernel/KernelBinaryKillSwitch.sol new file mode 100644 index 000000000..f3f3749b1 --- /dev/null +++ b/contracts/kill_switch/kernel/KernelBinaryKillSwitch.sol @@ -0,0 +1,18 @@ +pragma solidity 0.4.24; + +import "./KernelKillSwitch.sol"; +import "../base/BinaryKillSwitch.sol"; + + +contract KernelBinaryKillSwitch is KernelKillSwitch, BinaryKillSwitch { + constructor(bool _shouldPetrify) Kernel(_shouldPetrify) public { + // solium-disable-previous-line no-empty-blocks + } + + function setContractAction(address _contract, ContractAction _action) + external + auth(SET_CONTRACT_ACTION_ROLE, arr(_contract, msg.sender)) + { + _setContractAction(_contract, _action); + } +} diff --git a/contracts/kill_switch/kernel/KernelKillSwitch.sol b/contracts/kill_switch/kernel/KernelKillSwitch.sol new file mode 100644 index 000000000..8f2261509 --- /dev/null +++ b/contracts/kill_switch/kernel/KernelKillSwitch.sol @@ -0,0 +1,40 @@ +pragma solidity 0.4.24; + +import "../base/KillSwitch.sol"; +import "../../kernel/Kernel.sol"; + + +contract KernelKillSwitch is Kernel, KillSwitch { + string private constant ERROR_CONTRACT_CALL_NOT_ALLOWED = "KERNEL_CONTRACT_CALL_NOT_ALLOWED"; + + function initialize(IssuesRegistry _issuesRegistry, IACL _baseAcl, address _permissionsCreator) public onlyInit { + _setIssuesRegistry(_issuesRegistry); + Kernel.initialize(_baseAcl, _permissionsCreator); + } + + function getApp(bytes32 _namespace, bytes32 _appId) public view returns (address) { + // TODO: The tx information that the kill switch should eval cannot be accessed from here. + // Note that `msg.sender` is the proxy requesting the base app address, and `msg.data` + // refers to this call (`Kernel#getApp(bytes32,bytes32)`) + + address _app = super.getApp(_namespace, _appId); + bool _isCallAllowed = !shouldDenyCallingContract(_app, msg.sender, address(0), new bytes(0), uint256(0)); + require(_isCallAllowed, ERROR_CONTRACT_CALL_NOT_ALLOWED); + return _app; + } + + function _shouldEvaluateCall(address _base, address _instance, address _sender, bytes _data, uint256 _value) internal returns (bool) { + /****************************************** IMPORTANT *********************************************/ + /* Due to how proxies work, every time we call a proxied app, we will ask the kernel what's the */ + /* address of the base implementation where it should delegate the call to. But since the kernel */ + /* is also a proxy, it will basically delegate that query to the base kernel implementation, and */ + /* that's when this context is evaluated. Thus, we don't have full context of the call that its */ + /* about to be delegated to the base app implementation, the msg.data corresponds to the Kernel */ + /* getApp(bytes32,bytes32) method for example. Therefore, handling specific scenarios here it's */ + /* really cumbersome. We could rely easily on timestamps or block information, but tx data does */ + /* not correspond to the application call in this context. */ + /**************************************************************************************************/ + + return super._shouldEvaluateCall(_base, _instance, _sender, _data, _value); + } +} diff --git a/contracts/kill_switch/kernel/KernelSeveritiesKillSwitch.sol b/contracts/kill_switch/kernel/KernelSeveritiesKillSwitch.sol new file mode 100644 index 000000000..5dc83147c --- /dev/null +++ b/contracts/kill_switch/kernel/KernelSeveritiesKillSwitch.sol @@ -0,0 +1,25 @@ +pragma solidity 0.4.24; + +import "./KernelKillSwitch.sol"; +import "../base/SeveritiesKillSwitch.sol"; + + +contract KernelSeveritiesKillSwitch is KernelKillSwitch, SeveritiesKillSwitch { + constructor(bool _shouldPetrify) Kernel(_shouldPetrify) public { + // solium-disable-previous-line no-empty-blocks + } + + function setContractAction(address _contract, ContractAction _action) + external + auth(SET_CONTRACT_ACTION_ROLE, arr(_contract, msg.sender)) + { + _setContractAction(_contract, _action); + } + + function setLowestAllowedSeverity(address _contract, IssuesRegistry.Severity _severity) + external + auth(SET_LOWEST_ALLOWED_SEVERITY_ROLE, arr(_contract, msg.sender)) + { + _setLowestAllowedSeverity(_contract, _severity); + } +} diff --git a/contracts/test/mocks/kill_switch/app/AppKillSwitchedAppMock.sol b/contracts/test/mocks/kill_switch/app/AppKillSwitchedAppMock.sol new file mode 100644 index 000000000..2bce791c1 --- /dev/null +++ b/contracts/test/mocks/kill_switch/app/AppKillSwitchedAppMock.sol @@ -0,0 +1,27 @@ +pragma solidity 0.4.24; + +import "../../../../kill_switch/app/KillSwitchedApp.sol"; + + +contract AppKillSwitchedAppMock is KillSwitchedApp { + address public owner; + uint256 internal data; + + function initialize(AppKillSwitch _appKillSwitch, address _owner) public onlyInit { + super.initialize(_appKillSwitch); + data = 42; + owner = _owner; + } + + function read() public view returns (uint256) { + return data; + } + + function write(uint256 _data) public killSwitched { + data = _data; + } + + function reset() public killSwitched { + data = 0; + } +} diff --git a/contracts/test/mocks/kill_switch/app/BinaryKillSwitchedAppMock.sol b/contracts/test/mocks/kill_switch/app/BinaryKillSwitchedAppMock.sol new file mode 100644 index 000000000..d746aad91 --- /dev/null +++ b/contracts/test/mocks/kill_switch/app/BinaryKillSwitchedAppMock.sol @@ -0,0 +1,21 @@ +pragma solidity 0.4.24; + +import "./AppKillSwitchedAppMock.sol"; +import "../../../../kill_switch/app/AppBinaryKillSwitch.sol"; + + +contract AppBinaryKillSwitchMock is AppBinaryKillSwitch { + function _shouldEvaluateCall(address /*_base*/, address _instance, address _sender, bytes _data, uint256 /*_value*/) internal returns (bool) { + bytes4 methodID; + assembly { methodID := mload(add(_data, 0x20)) } + + // since this will act for every tx of the app, we provide a whitelist of functions + AppKillSwitchedAppMock app = AppKillSwitchedAppMock(_instance); + + // if called method is #reset, and the sender is the owner, do not evaluate + if (methodID == app.reset.selector && _sender == app.owner()) return false; + + // evaluate otherwise + return true; + } +} diff --git a/contracts/test/mocks/kill_switch/app/SeveritiesKillSwitchedAppMock.sol b/contracts/test/mocks/kill_switch/app/SeveritiesKillSwitchedAppMock.sol new file mode 100644 index 000000000..bbe682a1e --- /dev/null +++ b/contracts/test/mocks/kill_switch/app/SeveritiesKillSwitchedAppMock.sol @@ -0,0 +1,21 @@ +pragma solidity 0.4.24; + +import "./AppKillSwitchedAppMock.sol"; +import "../../../../kill_switch/app/AppSeveritiesKillSwitch.sol"; + + +contract AppSeveritiesKillSwitchMock is AppSeveritiesKillSwitch { + function _shouldEvaluateCall(address /*_base*/, address _instance, address _sender, bytes _data, uint256 /*_value*/) internal returns (bool) { + bytes4 methodID; + assembly { methodID := mload(add(_data, 0x20)) } + + // since this will act for every tx of the app, we provide a whitelist of functions + AppKillSwitchedAppMock app = AppKillSwitchedAppMock(_instance); + + // if called method is #reset, and the sender is the owner, do not evaluate + if (methodID == app.reset.selector && _sender == app.owner()) return false; + + // evaluate otherwise + return true; + } +} diff --git a/contracts/test/mocks/kill_switch/kernel/KernelKillSwitchAppMock.sol b/contracts/test/mocks/kill_switch/kernel/KernelKillSwitchAppMock.sol new file mode 100644 index 000000000..9bfec0be7 --- /dev/null +++ b/contracts/test/mocks/kill_switch/kernel/KernelKillSwitchAppMock.sol @@ -0,0 +1,27 @@ +pragma solidity 0.4.24; + +import "../../../../apps/AragonApp.sol"; + + +contract KernelKillSwitchAppMock is AragonApp { + address public owner; + uint256 internal data; + + function initialize(address _owner) public onlyInit { + initialized(); + data = 42; + owner = _owner; + } + + function read() public view returns (uint256) { + return data; + } + + function write(uint256 _data) public { + data = _data; + } + + function reset() public { + data = 0; + } +} diff --git a/test/kill_switch/app/AppBinaryKillSwitch.test.js b/test/kill_switch/app/AppBinaryKillSwitch.test.js new file mode 100644 index 000000000..7ae743735 --- /dev/null +++ b/test/kill_switch/app/AppBinaryKillSwitch.test.js @@ -0,0 +1,320 @@ +const { ACTION, SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { getEventArgument } = require('../helpers/events') +const itBehavesLikeBinaryKillSwitch = require('../base/itBehavesLikeBinaryKillSwitch') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KillSwitchedApp = artifacts.require('AppKillSwitchedAppMock') +const AppBinaryKillSwitch = artifacts.require('AppBinaryKillSwitchMock') + +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('AppBinaryKillSwitch', ([_, root, owner, securityPartner, anyone]) => { + let kernelBase, aclBase, appBase, appKillSwitchBase, issuesRegistryBase + let registryFactory, dao, acl, issuesRegistry, app, appKillSwitch + + before('deploy base implementations', async () => { + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + registryFactory = await EVMScriptRegistryFactory.new() + appKillSwitchBase = await AppBinaryKillSwitch.new() + issuesRegistryBase = await IssuesRegistry.new() + appBase = await KillSwitchedApp.new() + }) + + before('deploy DAO', async () => { + const daoFactory = await DAOFactory.new(kernelBase.address, aclBase.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 issuesRegistryReceipt = await dao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getEventArgument(issuesRegistryReceipt, 'NewAppProxy', 'proxy')) + await issuesRegistry.initialize() + const SET_ENTRY_SEVERITY_ROLE = await issuesRegistryBase.SET_ENTRY_SEVERITY_ROLE() + await acl.createPermission(securityPartner, issuesRegistry.address, SET_ENTRY_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create app kill switch', async () => { + const appKillSwitchReceipt = await dao.newAppInstance('0x1235', appKillSwitchBase.address, '0x', false, { from: root }) + appKillSwitch = AppBinaryKillSwitch.at(getEventArgument(appKillSwitchReceipt, 'NewAppProxy', 'proxy')) + await appKillSwitch.initialize(issuesRegistry.address) + const SET_CONTRACT_ACTION_ROLE = await appKillSwitchBase.SET_CONTRACT_ACTION_ROLE() + await acl.createPermission(owner, appKillSwitch.address, SET_CONTRACT_ACTION_ROLE, root, { from: root }) + }) + + beforeEach('create kill switched app', async () => { + const appReceipt = await dao.newAppInstance('0x1236', appBase.address, '0x', false, { from: root }) + app = KillSwitchedApp.at(getEventArgument(appReceipt, 'NewAppProxy', 'proxy')) + await app.initialize(appKillSwitch.address, owner) + }) + + describe('binary kill switch', function () { + beforeEach('bind kill switch', function () { + this.killSwitch = appKillSwitch + }) + + itBehavesLikeBinaryKillSwitch(owner, anyone) + }) + + describe('integration', () => { + context('when the function being called is not tagged', () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + } + + context('when there is no bug registered', () => { + context('when the contract being called is checked', () => { + itExecutesTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.LOW, { from: securityPartner }) + }) + + context('when the contract being called is checked', () => { + itExecutesTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + }) + + context('when the function being called is tagged', () => { + context('when the function being called is always evaluated', () => { + 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_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + const itExecutesTheCallWhenNotDenied = () => { + context('when the contract being called is checked', () => { + itExecutesTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + } + + context('when there is no bug registered', () => { + itExecutesTheCallWhenNotDenied() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.LOW, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall(owner) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + }) + }) + + context('when the function being called is evaluated only when the sender is not the owner', () => { + const itExecutesTheCall = (from) => { + it('executes the call', async () => { + await app.reset({ from }) + assert.equal(await app.read(), 0) + }) + } + + const itDoesNotExecuteTheCall = (from) => { + it('does not execute the call', async () => { + await assertRevert(app.reset({ from }), 'APP_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + const itExecutesTheCallEvenIfDenied = (from) => { + context('when the contract being called is checked', () => { + itExecutesTheCall(from) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(from) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itExecutesTheCall(from) + }) + } + + const itExecutesTheCallWhenNotDenied = (from) => { + context('when the contract being called is checked', () => { + itExecutesTheCall(from) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(from) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall(from) + }) + } + + const itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner = () => { + context('when the sender is the owner', () => { + itExecutesTheCallEvenIfDenied(owner) + }) + + context('when the sender is not the owner', () => { + itExecutesTheCallWhenNotDenied(anyone) + }) + } + + context('when there is no bug registered', () => { + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.LOW, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when the sender is the owner', () => { + itExecutesTheCallEvenIfDenied(owner) + }) + + context('when the sender is not the owner', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall(anyone) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + context('when the sender is not the owner', () => { + itExecutesTheCall(anyone) + }) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall(anyone) + }) + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + }) + }) + }) + }) +}) diff --git a/test/kill_switch/app/AppSeveritiesKillSwitch.test.js b/test/kill_switch/app/AppSeveritiesKillSwitch.test.js new file mode 100644 index 000000000..160080212 --- /dev/null +++ b/test/kill_switch/app/AppSeveritiesKillSwitch.test.js @@ -0,0 +1,507 @@ +const { ACTION, SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { getEventArgument } = require('../helpers/events') +const itBehavesLikeSeveritiesKillSwitch = require('../base/itBehavesLikeSeveritiesKillSwitch') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KillSwitchedApp = artifacts.require('AppKillSwitchedAppMock') +const AppSeveritiesKillSwitch = artifacts.require('AppSeveritiesKillSwitchMock') + +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('AppSeveritiesKillSwitch', ([_, root, owner, securityPartner, anyone]) => { + let kernelBase, aclBase, appBase, appKillSwitchBase, issuesRegistryBase + let registryFactory, dao, acl, issuesRegistry, app, appKillSwitch + + before('deploy base implementations', async () => { + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + registryFactory = await EVMScriptRegistryFactory.new() + appKillSwitchBase = await AppSeveritiesKillSwitch.new() + issuesRegistryBase = await IssuesRegistry.new() + appBase = await KillSwitchedApp.new() + }) + + before('deploy DAO', async () => { + const daoFactory = await DAOFactory.new(kernelBase.address, aclBase.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 issuesRegistryReceipt = await dao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getEventArgument(issuesRegistryReceipt, 'NewAppProxy', 'proxy')) + await issuesRegistry.initialize() + const SET_ENTRY_SEVERITY_ROLE = await issuesRegistryBase.SET_ENTRY_SEVERITY_ROLE() + await acl.createPermission(securityPartner, issuesRegistry.address, SET_ENTRY_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create app kill switch', async () => { + const appKillSwitchReceipt = await dao.newAppInstance('0x1235', appKillSwitchBase.address, '0x', false, { from: root }) + appKillSwitch = AppSeveritiesKillSwitch.at(getEventArgument(appKillSwitchReceipt, 'NewAppProxy', 'proxy')) + await appKillSwitch.initialize(issuesRegistry.address) + const SET_CONTRACT_ACTION_ROLE = await appKillSwitchBase.SET_CONTRACT_ACTION_ROLE() + await acl.createPermission(owner, appKillSwitch.address, SET_CONTRACT_ACTION_ROLE, root, { from: root }) + const SET_LOWEST_ALLOWED_SEVERITY_ROLE = await appKillSwitchBase.SET_LOWEST_ALLOWED_SEVERITY_ROLE() + await acl.createPermission(owner, appKillSwitch.address, SET_LOWEST_ALLOWED_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create kill switched app', async () => { + const appReceipt = await dao.newAppInstance('0x1236', appBase.address, '0x', false, { from: root }) + app = KillSwitchedApp.at(getEventArgument(appReceipt, 'NewAppProxy', 'proxy')) + await app.initialize(appKillSwitch.address, owner) + }) + + describe('binary kill switch', function () { + beforeEach('bind kill switch', function () { + this.killSwitch = appKillSwitch + }) + + itBehavesLikeSeveritiesKillSwitch(owner, anyone) + }) + + describe('integration', () => { + context('when the function being called is not tagged', () => { + + const itExecutesTheCallEvenIfDenied = () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + } + + context('when the contract being called is not denied', () => { + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itExecutesTheCall() + }) + } + + context('when there is no bug registered', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallEvenIfDenied() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallEvenIfDenied() + }) + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallEvenIfDenied() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + itExecutesTheCallEvenIfDenied() + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallEvenIfDenied() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallEvenIfDenied() + }) + }) + }) + }) + + context('when the function being called is tagged', () => { + describe('when the function being called is always evaluated', () => { + 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_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + const itExecutesTheCallWhenNotDenied = () => { + context('when the contract being called is checked', () => { + itExecutesTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + } + + context('when there is no bug registered', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallWhenNotDenied() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallWhenNotDenied() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + }) + }) + }) + }) + + describe('when the function being called is evaluated only when the sender is not the owner', () => { + const itExecutesTheCall = (from) => { + it('executes the call', async () => { + await app.reset({ from }) + assert.equal(await app.read(), 0) + }) + } + + const itDoesNotExecuteTheCall = (from) => { + it('does not execute the call', async () => { + await assertRevert(app.reset({ from }), 'APP_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + const itExecutesTheCallEvenIfDenied = (from) => { + context('when the contract being called is checked', () => { + itExecutesTheCall(from) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(from) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itExecutesTheCall(from) + }) + } + + const itExecutesTheCallWhenNotDenied = (from) => { + context('when the contract being called is checked', () => { + itExecutesTheCall(from) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(from) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall(from) + }) + } + + const itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner = () => { + context('when the sender is the owner', () => { + itExecutesTheCallEvenIfDenied(owner) + }) + + context('when the sender is not the owner', () => { + itExecutesTheCallWhenNotDenied(anyone) + }) + } + + context('when there is no bug registered', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + context('when the sender is the owner', () => { + itExecutesTheCallEvenIfDenied(owner) + }) + + context('when the sender is not the owner', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall(anyone) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(anyone) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall(anyone) + }) + }) + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + context('when the sender is the owner', () => { + itExecutesTheCallEvenIfDenied(owner) + }) + + context('when the sender is not the owner', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall(anyone) + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall(anyone) + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await appKillSwitch.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall(anyone) + }) + }) + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await appKillSwitch.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCallUnlessItsDeniedAndSenderIsNotOwner() + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/test/kill_switch/base/IssuesRegistry.test.js b/test/kill_switch/base/IssuesRegistry.test.js new file mode 100644 index 000000000..bdc78d618 --- /dev/null +++ b/test/kill_switch/base/IssuesRegistry.test.js @@ -0,0 +1,129 @@ +const { SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { getEventArgument } = require('../helpers/events') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const ACL = artifacts.require('ACL') +const Kernel = artifacts.require('Kernel') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('IssuesRegistry', ([_, root, implementation, owner, anyone]) => { + let kernelBase, aclBase, issuesRegistryBase, registryFactory, dao, acl, issuesRegistry + + before('deploy base implementations', async () => { + kernelBase = await Kernel.new(true) // petrify immediately + aclBase = await ACL.new() + registryFactory = await EVMScriptRegistryFactory.new() + issuesRegistryBase = await IssuesRegistry.new() + }) + + before('deploy DAO', async () => { + const daoFactory = await DAOFactory.new(kernelBase.address, aclBase.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 issuesRegistryReceipt = await dao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getEventArgument(issuesRegistryReceipt, 'NewAppProxy', 'proxy')) + await issuesRegistry.initialize() + const SET_ENTRY_SEVERITY_ROLE = await issuesRegistryBase.SET_ENTRY_SEVERITY_ROLE() + await acl.createPermission(owner, issuesRegistry.address, SET_ENTRY_SEVERITY_ROLE, root, { from: root }) + }) + + describe('isSeverityFor', () => { + context('when there was no severity set before', () => { + it('returns false', async () => { + assert.isFalse(await issuesRegistry.isSeverityFor(implementation), 'did not expect severity for given entry') + }) + }) + + context('when there was a severity already set', () => { + beforeEach('set medium severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from: owner }) + }) + + context('when the issues was not fixed yet', () => { + it('returns true', async () => { + assert.isTrue(await issuesRegistry.isSeverityFor(implementation), 'did not expect severity for given entry') + }) + }) + + context('when the issues was already fixed', () => { + beforeEach('set medium severity', async () => { + await issuesRegistry.setSeverityFor(implementation, SEVERITY.NONE, { from: owner }) + }) + + it('returns false', async () => { + assert.isFalse(await issuesRegistry.isSeverityFor(implementation), 'did not expect severity for given entry') + }) + }) + }) + }) + + 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 { logs } = await issuesRegistry.setSeverityFor(implementation, SEVERITY.LOW, { from }) + + const events = logs.filter(l => l.event === 'SeveritySet') + assert.equal(events.length, 1, 'number of SeveritySet events does not match') + assert.equal(events[0].args.entry, implementation, 'entry address does not match') + assert.equal(events[0].args.severity, SEVERITY.LOW, 'severity does not match') + assert.equal(events[0].args.sender, owner, 'sender does not match') + }) + + context('when there was no severity set before', () => { + it('sets the severity for the given entry', 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 entry', 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/kill_switch/base/itBehavesLikeBinaryKillSwitch.js b/test/kill_switch/base/itBehavesLikeBinaryKillSwitch.js new file mode 100644 index 000000000..043845b39 --- /dev/null +++ b/test/kill_switch/base/itBehavesLikeBinaryKillSwitch.js @@ -0,0 +1,86 @@ +const { ACTION, SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') + +module.exports = function (owner, anAddress) { + describe('isContractIgnored', function () { + context('when the contract is checked', function () { + it('returns false', async function () { + assert.isFalse(await this.killSwitch.isContractIgnored(anAddress)) + }) + }) + + context('when the contract is ignored', function () { + beforeEach('ignore contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.IGNORE, { from: owner }) + }) + + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isContractIgnored(anAddress)) + }) + }) + }) + + describe('isContractDenied', function () { + context('when the contract is not denied', function () { + it('returns false', async function () { + assert.isFalse(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + + context('when the contract is ignored', function () { + beforeEach('ignore contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from: owner }) + }) + + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + }) + + describe('setContractAction', function () { + context('when the sender is the owner', function () { + const from = owner + + context('when there was no action set yet', function () { + it('sets a new action', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from }) + + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + + context('when there was an action already set', function () { + beforeEach('deny contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from }) + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + + it('changes the contract action', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.IGNORE, { from }) + + assert.isTrue(await this.killSwitch.isContractIgnored(anAddress)) + assert.isFalse(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + }) + + context('when the sender is not the owner', function () { + it('reverts', async function () { + await assertRevert(this.killSwitch.setContractAction(anAddress, ACTION.DENY)) + }) + }) + }) + + describe('isSeverityIgnored', function () { + it('returns true for none severities', async function () { + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.NONE)) + }) + + it('returns false for all the severities', async function () { + for (const key of Object.keys(SEVERITY).slice(1)) { + assert.isFalse(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY[key])) + } + }) + }) +} diff --git a/test/kill_switch/base/itBehavesLikeSeveritiesKillSwitch.js b/test/kill_switch/base/itBehavesLikeSeveritiesKillSwitch.js new file mode 100644 index 000000000..d6e8f0730 --- /dev/null +++ b/test/kill_switch/base/itBehavesLikeSeveritiesKillSwitch.js @@ -0,0 +1,143 @@ +const { ACTION, SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') + +module.exports = function (owner, anAddress) { + describe('isContractIgnored', function () { + context('when the contract is checked', function () { + it('returns false', async function () { + assert.isFalse(await this.killSwitch.isContractIgnored(anAddress)) + }) + }) + + context('when the contract is ignored', function () { + beforeEach('ignore contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.IGNORE, { from: owner }) + }) + + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isContractIgnored(anAddress)) + }) + }) + }) + + describe('isContractDenied', function () { + context('when the contract is not denied', function () { + it('returns false', async function () { + assert.isFalse(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + + context('when the contract is ignored', function () { + beforeEach('ignore contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from: owner }) + }) + + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + }) + + describe('setContractAction', function () { + context('when the sender is the owner', function () { + const from = owner + + context('when there was no action set yet', function () { + it('sets a new action', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from }) + + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + + context('when there was an action already set', function () { + beforeEach('deny contract', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.DENY, { from }) + assert.isTrue(await this.killSwitch.isContractDenied(anAddress)) + }) + + it('changes the contract action', async function () { + await this.killSwitch.setContractAction(anAddress, ACTION.IGNORE, { from }) + + assert.isTrue(await this.killSwitch.isContractIgnored(anAddress)) + assert.isFalse(await this.killSwitch.isContractDenied(anAddress)) + }) + }) + }) + + context('when the sender is not the owner', function () { + it('reverts', async function () { + await assertRevert(this.killSwitch.setContractAction(anAddress, ACTION.DENY)) + }) + }) + }) + + describe('isSeverityIgnored', function () { + context('when no lowest allowed severity was set yet', function () { + it('returns false for all the severities', async function () { + for (const key of Object.keys(SEVERITY).slice(1)) { + assert.isFalse(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY[key])) + } + }) + }) + + context('when a lowest allowed severity was set', function () { + beforeEach('set a lowest allowed severity', async function () { + await this.killSwitch.setLowestAllowedSeverity(anAddress, SEVERITY.MID, { from: owner }) + }) + + context('when the given severity is lower than the one set', function () { + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.LOW)) + }) + }) + + context('when the given severity is equal to the one set', function () { + it('returns true', async function () { + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.MID)) + }) + }) + + context('when the given severity is greater than the one set', function () { + it('returns false', async function () { + assert.isFalse(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.HIGH)) + }) + }) + }) + }) + + describe('setLowestAllowedSeverity', function () { + context('when the contract is the owner', function () { + const from = owner + + context('when there was no severity set', function () { + it('sets the lowest allowed severity', async function () { + await this.killSwitch.setLowestAllowedSeverity(anAddress, SEVERITY.HIGH, { from }) + + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.HIGH)) + assert.isFalse(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.CRITICAL)) + }) + }) + + context('when there was a previous severity set', function () { + beforeEach('set lowest allowed severity', async function () { + await this.killSwitch.setLowestAllowedSeverity(anAddress, SEVERITY.LOW, { from }) + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.LOW)) + }) + + it('changes the lowest allowed severity', async function () { + await this.killSwitch.setLowestAllowedSeverity(anAddress, SEVERITY.MID, { from }) + + assert.isTrue(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.MID)) + assert.isFalse(await this.killSwitch.isSeverityIgnored(anAddress, SEVERITY.HIGH)) + }) + }) + }) + + context('when the sender is not the owner', function () { + it('reverts', async function () { + await assertRevert(this.killSwitch.setLowestAllowedSeverity(anAddress, SEVERITY.MID)) + }) + }) + }) +} diff --git a/test/kill_switch/helpers/enums.js b/test/kill_switch/helpers/enums.js new file mode 100644 index 000000000..01d118b73 --- /dev/null +++ b/test/kill_switch/helpers/enums.js @@ -0,0 +1,7 @@ +const ACTION = { CHECK: 0, IGNORE: 1, DENY: 2 } +const SEVERITY = { NONE: 0, LOW: 1, MID: 2, HIGH: 3, CRITICAL: 4 } + +module.exports = { + ACTION, + SEVERITY +} diff --git a/test/kill_switch/helpers/events.js b/test/kill_switch/helpers/events.js new file mode 100644 index 000000000..de6e1b63a --- /dev/null +++ b/test/kill_switch/helpers/events.js @@ -0,0 +1,5 @@ +const getEventArgument = (receipt, event, arg) => receipt.logs.find(l => l.event === event).args[arg] + +module.exports = { + getEventArgument +} diff --git a/test/kill_switch/kernel/KernelBinaryKillSwitch.test.js b/test/kill_switch/kernel/KernelBinaryKillSwitch.test.js new file mode 100644 index 000000000..91be1ff6b --- /dev/null +++ b/test/kill_switch/kernel/KernelBinaryKillSwitch.test.js @@ -0,0 +1,147 @@ +const { assertRevert } = require('../../helpers/assertThrow') +const { ACTION, SEVERITY } = require('../helpers/enums') +const { getEventArgument } = require('../helpers/events') +const itBehavesLikeBinaryKillSwitch = require('../base/itBehavesLikeBinaryKillSwitch') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KernelKillSwitchAppMock = artifacts.require('KernelKillSwitchAppMock') + +const ACL = artifacts.require('ACL') +const RegularKernel = artifacts.require('Kernel') +const KernelKillSwitch = artifacts.require('KernelBinaryKillSwitch') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('KernelBinaryKillSwitch', ([_, root, owner, securityPartner, anyone]) => { + let killSwitchedKernelBase, regularKernelBase, aclBase, appBase, issuesRegistryBase, registryFactory + let regularDao, regularAcl, killSwitchedDao, killSwitchedAcl, issuesRegistry, app + + before('deploy base implementations', async () => { + regularKernelBase = await RegularKernel.new(true) // petrify immediately + killSwitchedKernelBase = await KernelKillSwitch.new(true) // petrify immediately + aclBase = await ACL.new() + appBase = await KernelKillSwitchAppMock.new() + issuesRegistryBase = await IssuesRegistry.new() + registryFactory = await EVMScriptRegistryFactory.new() + }) + + beforeEach('deploy DAO with regular kernel', async () => { + const regularDaoFactory = await DAOFactory.new(regularKernelBase.address, aclBase.address, registryFactory.address) + const regularKernelReceipt = await regularDaoFactory.newDAO(root) + regularDao = RegularKernel.at(getEventArgument(regularKernelReceipt, 'DeployDAO', 'dao')) + regularAcl = ACL.at(await regularDao.acl()) + + const APP_MANAGER_ROLE = await regularKernelBase.APP_MANAGER_ROLE() + await regularAcl.createPermission(root, regularDao.address, APP_MANAGER_ROLE, root, { from: root }) + }) + + beforeEach('create issues registry app from DAO with regular kernel', async () => { + const issuesRegistryReceipt = await regularDao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getEventArgument(issuesRegistryReceipt, 'NewAppProxy', 'proxy')) + await issuesRegistry.initialize() + const SET_ENTRY_SEVERITY_ROLE = await issuesRegistryBase.SET_ENTRY_SEVERITY_ROLE() + await regularAcl.createPermission(securityPartner, issuesRegistry.address, SET_ENTRY_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('deploy DAO with kernel binary kill switch', async () => { + const killSwitchedDaoFactory = await DAOFactory.new(killSwitchedKernelBase.address, aclBase.address, registryFactory.address) + const killSwitchedKernelReceipt = await killSwitchedDaoFactory.newDAOWithKillSwitch(root, issuesRegistry.address) + killSwitchedDao = KernelKillSwitch.at(getEventArgument(killSwitchedKernelReceipt, 'DeployDAO', 'dao')) + killSwitchedAcl = ACL.at(await killSwitchedDao.acl()) + + const APP_MANAGER_ROLE = await killSwitchedKernelBase.APP_MANAGER_ROLE() + await killSwitchedAcl.createPermission(root, killSwitchedDao.address, APP_MANAGER_ROLE, root, { from: root }) + const SET_CONTRACT_ACTION_ROLE = await killSwitchedDao.SET_CONTRACT_ACTION_ROLE() + await killSwitchedAcl.createPermission(owner, killSwitchedDao.address, SET_CONTRACT_ACTION_ROLE, root, { from: root }) + }) + + beforeEach('create sample app from DAO with kernel binary kill switch', async () => { + const appReceipt = await killSwitchedDao.newAppInstance('0x1235', appBase.address, '0x', false, { from: root }) + app = KernelKillSwitchAppMock.at(getEventArgument(appReceipt, 'NewAppProxy', 'proxy')) + await app.initialize(owner) + }) + + describe('binary kill switch', function () { + beforeEach('bind kill switch', function () { + this.killSwitch = killSwitchedDao + }) + + itBehavesLikeBinaryKillSwitch(owner, anyone) + }) + + describe('integration', () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + } + + const itDoesNotExecuteTheCall = () => { + it('does not execute the call', async () => { + await assertRevert(app.read(), 'KERNEL_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + const itExecutesTheCallWhenNotDenied = () => { + context('when the contract being called is checked', () => { + itExecutesTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await killSwitchedDao.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await killSwitchedDao.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + } + + context('when there is no bug registered', () => { + itExecutesTheCallWhenNotDenied() + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.LOW, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when the contract being called is checked', () => { + itDoesNotExecuteTheCall() + }) + + context('when the contract being called is ignored', () => { + beforeEach('ignore calling contract', async () => { + await killSwitchedDao.setContractAction(appBase.address, ACTION.IGNORE, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the contract being called is denied', () => { + beforeEach('deny calling contract', async () => { + await killSwitchedDao.setContractAction(appBase.address, ACTION.DENY, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + itExecutesTheCallWhenNotDenied() + }) + }) + }) +}) diff --git a/test/kill_switch/kernel/KernelSeveritiesKillSwitch.test.js b/test/kill_switch/kernel/KernelSeveritiesKillSwitch.test.js new file mode 100644 index 000000000..6165b5dff --- /dev/null +++ b/test/kill_switch/kernel/KernelSeveritiesKillSwitch.test.js @@ -0,0 +1,175 @@ +const { ACTION, SEVERITY } = require('../helpers/enums') +const { assertRevert } = require('../../helpers/assertThrow') +const { getEventArgument } = require('../helpers/events') +const itBehavesLikeSeveritiesKillSwitch = require('../base/itBehavesLikeSeveritiesKillSwitch') + +const IssuesRegistry = artifacts.require('IssuesRegistry') +const KernelKillSwitchAppMock = artifacts.require('KernelKillSwitchAppMock') + +const ACL = artifacts.require('ACL') +const RegularKernel = artifacts.require('Kernel') +const KernelKillSwitch = artifacts.require('KernelSeveritiesKillSwitch') +const DAOFactory = artifacts.require('DAOFactory') +const EVMScriptRegistryFactory = artifacts.require('EVMScriptRegistryFactory') + +contract('KernelSeveritiesKillSwitch', ([_, root, owner, securityPartner, anyone]) => { + let killSwitchedKernelBase, regularKernelBase, aclBase, appBase, issuesRegistryBase, registryFactory + let regularDao, regularAcl, killSwitchedDao, killSwitchedAcl, issuesRegistry, app + + before('deploy base implementations', async () => { + regularKernelBase = await RegularKernel.new(true) // petrify immediately + killSwitchedKernelBase = await KernelKillSwitch.new(true) // petrify immediately + aclBase = await ACL.new() + appBase = await KernelKillSwitchAppMock.new() + issuesRegistryBase = await IssuesRegistry.new() + registryFactory = await EVMScriptRegistryFactory.new() + }) + + before('deploy DAO with regular kernel', async () => { + const regularDaoFactory = await DAOFactory.new(regularKernelBase.address, aclBase.address, registryFactory.address) + const regularKernelReceipt = await regularDaoFactory.newDAO(root) + regularDao = RegularKernel.at(getEventArgument(regularKernelReceipt, 'DeployDAO', 'dao')) + regularAcl = ACL.at(await regularDao.acl()) + + const APP_MANAGER_ROLE = await regularKernelBase.APP_MANAGER_ROLE() + await regularAcl.createPermission(root, regularDao.address, APP_MANAGER_ROLE, root, { from: root }) + }) + + beforeEach('create issues registry app from DAO with regular kernel', async () => { + const issuesRegistryReceipt = await regularDao.newAppInstance('0x1234', issuesRegistryBase.address, '0x', false, { from: root }) + issuesRegistry = IssuesRegistry.at(getEventArgument(issuesRegistryReceipt, 'NewAppProxy', 'proxy')) + await issuesRegistry.initialize() + const SET_ENTRY_SEVERITY_ROLE = await issuesRegistryBase.SET_ENTRY_SEVERITY_ROLE() + await regularAcl.createPermission(securityPartner, issuesRegistry.address, SET_ENTRY_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('deploy DAO with kernel severities kill switch', async () => { + const killSwitchedDaoFactory = await DAOFactory.new(killSwitchedKernelBase.address, aclBase.address, registryFactory.address) + const killSwitchedKernelReceipt = await killSwitchedDaoFactory.newDAOWithKillSwitch(root, issuesRegistry.address) + killSwitchedDao = KernelKillSwitch.at(getEventArgument(killSwitchedKernelReceipt, 'DeployDAO', 'dao')) + killSwitchedAcl = ACL.at(await killSwitchedDao.acl()) + + const APP_MANAGER_ROLE = await killSwitchedKernelBase.APP_MANAGER_ROLE() + await killSwitchedAcl.createPermission(root, killSwitchedDao.address, APP_MANAGER_ROLE, root, { from: root }) + const SET_CONTRACT_ACTION_ROLE = await killSwitchedDao.SET_CONTRACT_ACTION_ROLE() + await killSwitchedAcl.createPermission(owner, killSwitchedDao.address, SET_CONTRACT_ACTION_ROLE, root, { from: root }) + const SET_LOWEST_ALLOWED_SEVERITY_ROLE = await killSwitchedDao.SET_LOWEST_ALLOWED_SEVERITY_ROLE() + await killSwitchedAcl.createPermission(owner, killSwitchedDao.address, SET_LOWEST_ALLOWED_SEVERITY_ROLE, root, { from: root }) + }) + + beforeEach('create sample app from DAO with kernel severities kill switch', async () => { + const appReceipt = await killSwitchedDao.newAppInstance('0x1235', appBase.address, '0x', false, { from: root }) + app = KernelKillSwitchAppMock.at(getEventArgument(appReceipt, 'NewAppProxy', 'proxy')) + await app.initialize(owner) + }) + + describe('severities kill switch', function () { + beforeEach('bind kill switch', function () { + this.killSwitch = killSwitchedDao + }) + + itBehavesLikeSeveritiesKillSwitch(owner, anyone) + }) + + describe('integration', () => { + const itExecutesTheCall = () => { + it('executes the call', async () => { + assert.equal(await app.read(), 42) + }) + } + + const itDoesNotExecuteTheCall = () => { + it('does not execute the call', async () => { + await assertRevert(app.read(), 'KERNEL_CONTRACT_CALL_NOT_ALLOWED') + }) + } + + context('when there is no bug registered', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCall() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + + context('when there is a bug registered', () => { + beforeEach('register a bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.MID, { from: securityPartner }) + }) + + context('when the bug was not fixed yet', () => { + context('when there is no lowest allowed severity set for the contract being called', () => { + itDoesNotExecuteTheCall() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itDoesNotExecuteTheCall() + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + }) + + context('when the bug was already fixed', () => { + beforeEach('fix bug', async () => { + await issuesRegistry.setSeverityFor(appBase.address, SEVERITY.NONE, { from: securityPartner }) + }) + + context('when there is no lowest allowed severity set for the contract being called', () => { + itExecutesTheCall() + }) + + context('when there is a lowest allowed severity set for the contract being called', () => { + context('when the lowest allowed severity is under the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.LOW, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the lowest allowed severity is equal to the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.MID, { from: owner }) + }) + + itExecutesTheCall() + }) + + context('when the lowest allowed severity is greater than the reported bug severity', () => { + beforeEach('set lowest allowed severity', async () => { + await killSwitchedDao.setLowestAllowedSeverity(appBase.address, SEVERITY.CRITICAL, { from: owner }) + }) + + itExecutesTheCall() + }) + }) + }) + }) + }) +})