From 3d484e5a3232fcb0d345476ab375d55a19db0bf2 Mon Sep 17 00:00:00 2001 From: Giorgi Lagidze Date: Wed, 30 Oct 2024 14:44:52 +0400 Subject: [PATCH] add condition (#108) * add condition * rename * fix linter * fix: executor 63/64 rule tests for coverage * feat: add tests for powerful condition contract * ci: undo changes * ci: rmv not used import * fix: prettier * feat: add tests for conditions that checks block number and timestamp * ci: prettier * ci: lint fix * Feat/rule condition clean up (#109) * cd: rename powerfulCondtion to RuledCondition * add natspecs * remove redandant imports * extension alwaystruecondition * extension folder * rename, fix and add tests * add rules updated event * Feat: add missing tests (#111) * feat: add test for always true condition * feat: modify mock code to allow sending the compare list on the data * feat: add tests for rule condition * feat: simplify ruled condition tests * fix lint * add supportsinterface on executor * change const name --------- Co-authored-by: Claudia Co-authored-by: Rekard0 <5880388+Rekard0@users.noreply.github.com> --- contracts/src/executors/Executor.sol | 19 +- .../src/mocks/executors/ActionExecute.sol | 10 +- contracts/src/mocks/executors/GasConsumer.sol | 1 + .../extensions/RuledConditionMock.sol | 25 + .../src/mocks/plugin/CustomExecutorMock.sol | 1 - .../src/mocks/plugin/PluginCloneableMock.sol | 2 +- contracts/src/mocks/plugin/PluginMock.sol | 2 +- .../plugin/PluginUUPSUpgradeableMock.sol | 2 +- .../extensions/proposal/ProposalMock.sol | 2 +- .../proposal/ProposalUpgradeableMock.sol | 2 +- .../extensions/AlwaysTrueCondition.sol | 16 + .../condition/extensions/RuledCondition.sol | 335 ++++++ .../metadata/MetadataExtensionUpgradeable.sol | 2 + contracts/test/executors/executor.ts | 38 +- .../extensions/always-true-condition.ts | 42 + .../condition/extensions/ruled-condition.ts | 1014 +++++++++++++++++ contracts/test/utils/condition/condition.ts | 25 + 17 files changed, 1512 insertions(+), 26 deletions(-) create mode 100644 contracts/src/mocks/permission/condition/extensions/RuledConditionMock.sol create mode 100644 contracts/src/permission/condition/extensions/AlwaysTrueCondition.sol create mode 100644 contracts/src/permission/condition/extensions/RuledCondition.sol create mode 100644 contracts/test/permission/condition/extensions/always-true-condition.ts create mode 100644 contracts/test/permission/condition/extensions/ruled-condition.ts create mode 100644 contracts/test/utils/condition/condition.ts diff --git a/contracts/src/executors/Executor.sol b/contracts/src/executors/Executor.sol index 96f8f841..21c97a31 100644 --- a/contracts/src/executors/Executor.sol +++ b/contracts/src/executors/Executor.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.8; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + import {IExecutor, Action} from "./IExecutor.sol"; import {flipBit, hasBit} from "../utils/math/BitMap.sol"; @@ -10,12 +12,12 @@ import {flipBit, hasBit} from "../utils/math/BitMap.sol"; /// Most useful use-case is to deploy as non-upgradeable and call from another contract via delegatecall. /// If used with delegatecall, DO NOT add state variables in sequential slots, otherwise this will overwrite /// the storage of the calling contract. -contract Executor is IExecutor { +contract Executor is IExecutor, ERC165 { /// @notice The internal constant storing the maximal action array length. uint256 internal constant MAX_ACTIONS = 256; // keccak256("osx-commons.storage.Executor") - bytes32 private constant ReentrancyGuardStorageLocation = + bytes32 private constant REENTRANCY_GUARD_STORAGE_LOCATION = 0x4d6542319dfb3f7c8adbb488d7b4d7cf849381f14faf4b64de3ac05d08c0bdec; /// @notice The first out of two values to which the `_reentrancyStatus` state variable (used by the `nonReentrant` modifier) can be set indicating that a function was not entered. @@ -53,6 +55,13 @@ contract Executor is IExecutor { _storeReentrancyStatus(_NOT_ENTERED); } + /// @notice Checks if this or the parent contract supports an interface by its ID. + /// @param _interfaceId The ID of the interface. + /// @return Returns `true` if the interface is supported. + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return _interfaceId == type(IExecutor).interfaceId || super.supportsInterface(_interfaceId); + } + /// @inheritdoc IExecutor function execute( bytes32 _callId, @@ -120,15 +129,17 @@ contract Executor is IExecutor { /// @notice Gets the current reentrancy status. /// @return status This returns the current reentrancy status. function _getReentrancyStatus() private view returns (uint256 status) { + // solhint-disable-next-line no-inline-assembly assembly { - status := sload(ReentrancyGuardStorageLocation) + status := sload(REENTRANCY_GUARD_STORAGE_LOCATION) } } /// @notice Stores the reentrancy status on a specific slot. function _storeReentrancyStatus(uint256 _status) private { + // solhint-disable-next-line no-inline-assembly assembly { - sstore(ReentrancyGuardStorageLocation, _status) + sstore(REENTRANCY_GUARD_STORAGE_LOCATION, _status) } } } diff --git a/contracts/src/mocks/executors/ActionExecute.sol b/contracts/src/mocks/executors/ActionExecute.sol index 36fcd754..4892a87e 100644 --- a/contracts/src/mocks/executors/ActionExecute.sol +++ b/contracts/src/mocks/executors/ActionExecute.sol @@ -3,18 +3,18 @@ pragma solidity ^0.8.8; import {IExecutor, Action} from "../../executors/Executor.sol"; -import "hardhat/console.sol"; /// @notice A dummy contract to test if Executor can successfully execute an action. contract ActionExecute { - uint num = 10; + uint256 internal _num = 10; - function setTest(uint newNum) public returns (uint) { - num = newNum; - return num; + function setTest(uint256 newNum) public returns (uint256) { + _num = newNum; + return _num; } function fail() public pure { + // solhint-disable-next-line reason-string, custom-errors revert("ActionExecute:Revert"); } diff --git a/contracts/src/mocks/executors/GasConsumer.sol b/contracts/src/mocks/executors/GasConsumer.sol index 1eb2db57..4147df35 100644 --- a/contracts/src/mocks/executors/GasConsumer.sol +++ b/contracts/src/mocks/executors/GasConsumer.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.8; /// @notice This contract is used for testing to consume gas. contract GasConsumer { + // solhint-disable-next-line named-parameters-mapping mapping(uint256 => uint256) public store; function consumeGas(uint256 count) external { diff --git a/contracts/src/mocks/permission/condition/extensions/RuledConditionMock.sol b/contracts/src/mocks/permission/condition/extensions/RuledConditionMock.sol new file mode 100644 index 00000000..dce14e71 --- /dev/null +++ b/contracts/src/mocks/permission/condition/extensions/RuledConditionMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; +import {RuledCondition} from "../../../../permission/condition/extensions/RuledCondition.sol"; +import {DaoAuthorizableUpgradeable} from "../../../../permission/auth/DaoAuthorizableUpgradeable.sol"; + +/// @notice A mock powerful condition to expose internal functions +/// @dev DO NOT USE IN PRODUCTION! +contract RuledConditionMock is DaoAuthorizableUpgradeable, RuledCondition { + function updateRules(Rule[] memory _rules) public virtual { + _updateRules(_rules); + } + + function isGranted( + address _where, + address _who, + bytes32 _permissionId, + bytes calldata data + ) external view override returns (bool isPermitted) { + uint256[] memory _compareList = data.length == 0 + ? new uint256[](0) + : abi.decode(data, (uint256[])); + return _evalRule(0, _where, _who, _permissionId, _compareList); + } +} diff --git a/contracts/src/mocks/plugin/CustomExecutorMock.sol b/contracts/src/mocks/plugin/CustomExecutorMock.sol index acab86e9..0b8abe5d 100644 --- a/contracts/src/mocks/plugin/CustomExecutorMock.sol +++ b/contracts/src/mocks/plugin/CustomExecutorMock.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.8; -import {IDAO} from "../../dao/IDAO.sol"; import {IExecutor, Action} from "../../executors/IExecutor.sol"; /// @notice A mock DAO that anyone can set permissions in. diff --git a/contracts/src/mocks/plugin/PluginCloneableMock.sol b/contracts/src/mocks/plugin/PluginCloneableMock.sol index 2a5a4ecd..e3f6431c 100644 --- a/contracts/src/mocks/plugin/PluginCloneableMock.sol +++ b/contracts/src/mocks/plugin/PluginCloneableMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.8; import {PluginCloneable} from "../../plugin/PluginCloneable.sol"; import {IDAO} from "../../dao/IDAO.sol"; -import {IExecutor, Action} from "../../executors/IExecutor.sol"; +import {Action} from "../../executors/IExecutor.sol"; /// @notice A mock cloneable plugin to be deployed via the minimal proxy pattern. /// v1.1 (Release 1, Build 1) diff --git a/contracts/src/mocks/plugin/PluginMock.sol b/contracts/src/mocks/plugin/PluginMock.sol index 22fa5cc5..b23f0f7b 100644 --- a/contracts/src/mocks/plugin/PluginMock.sol +++ b/contracts/src/mocks/plugin/PluginMock.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.8; import {Plugin} from "../../plugin/Plugin.sol"; import {IDAO} from "../../dao/IDAO.sol"; -import {IExecutor, Action} from "../../executors/IExecutor.sol"; +import {Action} from "../../executors/IExecutor.sol"; /// @notice A mock plugin to be deployed via the `new` keyword. /// v1.1 (Release 1, Build 1) diff --git a/contracts/src/mocks/plugin/PluginUUPSUpgradeableMock.sol b/contracts/src/mocks/plugin/PluginUUPSUpgradeableMock.sol index a792458c..83289b87 100644 --- a/contracts/src/mocks/plugin/PluginUUPSUpgradeableMock.sol +++ b/contracts/src/mocks/plugin/PluginUUPSUpgradeableMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.8; import {PluginUUPSUpgradeable} from "../../plugin/PluginUUPSUpgradeable.sol"; import {IDAO} from "../../dao/IDAO.sol"; -import {IExecutor, Action} from "../../executors/IExecutor.sol"; +import {Action} from "../../executors/IExecutor.sol"; /// @notice A mock upgradeable plugin to be deployed via the UUPS proxy pattern. /// v1.1 (Release 1, Build 1) diff --git a/contracts/src/mocks/plugin/extensions/proposal/ProposalMock.sol b/contracts/src/mocks/plugin/extensions/proposal/ProposalMock.sol index 23d79b54..6af16ee6 100644 --- a/contracts/src/mocks/plugin/extensions/proposal/ProposalMock.sol +++ b/contracts/src/mocks/plugin/extensions/proposal/ProposalMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.8; import {Proposal} from "../../../../plugin/extensions/proposal/Proposal.sol"; -import {IExecutor, Action} from "../../../../executors/IExecutor.sol"; +import {Action} from "../../../../executors/IExecutor.sol"; /// @notice A mock contract. /// @dev DO NOT USE IN PRODUCTION! diff --git a/contracts/src/mocks/plugin/extensions/proposal/ProposalUpgradeableMock.sol b/contracts/src/mocks/plugin/extensions/proposal/ProposalUpgradeableMock.sol index 916a650f..4f1fdcd8 100644 --- a/contracts/src/mocks/plugin/extensions/proposal/ProposalUpgradeableMock.sol +++ b/contracts/src/mocks/plugin/extensions/proposal/ProposalUpgradeableMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.8; import {ProposalUpgradeable} from "../../../../plugin/extensions/proposal/ProposalUpgradeable.sol"; -import {IExecutor, Action} from "../../../../executors/IExecutor.sol"; +import {Action} from "../../../../executors/IExecutor.sol"; /// @notice A mock contract. /// @dev DO NOT USE IN PRODUCTION! diff --git a/contracts/src/permission/condition/extensions/AlwaysTrueCondition.sol b/contracts/src/permission/condition/extensions/AlwaysTrueCondition.sol new file mode 100644 index 00000000..3a07dbf9 --- /dev/null +++ b/contracts/src/permission/condition/extensions/AlwaysTrueCondition.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.8; + +import {PermissionCondition} from "../PermissionCondition.sol"; + +contract AlwaysTrueCondition is PermissionCondition { + function isGranted( + address _where, + address _who, + bytes32 _permissionId, + bytes calldata _data + ) public pure override returns (bool) { + (_where, _who, _permissionId, _data); + return true; + } +} diff --git a/contracts/src/permission/condition/extensions/RuledCondition.sol b/contracts/src/permission/condition/extensions/RuledCondition.sol new file mode 100644 index 00000000..af59068a --- /dev/null +++ b/contracts/src/permission/condition/extensions/RuledCondition.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {IPermissionCondition} from "../IPermissionCondition.sol"; +import {PermissionConditionUpgradeable} from "../PermissionConditionUpgradeable.sol"; + +/// @title RuledCondition +/// @author Aragon X - 2024 +/// @notice An abstract contract to create conditional permissions using rules. +abstract contract RuledCondition is PermissionConditionUpgradeable { + /// @notice Identifier for a rule based on the current block number. + uint8 internal constant BLOCK_NUMBER_RULE_ID = 200; + + /// @notice Identifier for a rule based on the current timestamp. + uint8 internal constant TIMESTAMP_RULE_ID = 201; + + /// @notice Identifier for a rule that evaluates a condition based on another condition contract. + uint8 internal constant CONDITION_RULE_ID = 202; + + /// @notice Identifier for a rule that is based on logical operations (e.g., AND, OR). + uint8 internal constant LOGIC_OP_RULE_ID = 203; + + /// @notice Identifier for a rule that involves direct value comparison. + uint8 internal constant VALUE_RULE_ID = 204; + + /// @notice Emitted when the rules are updated. + /// @param rules The new rules that replaces old rules. + event RulesUpdated(Rule[] rules); + + /// @notice Represents a rule used in the condition contract. + /// @param id The ID representing the identifier of the rule. + /// @param op The operation to apply, as defined in the `Op` enum. + /// @param value The value associated with this rule, which could be an address, timestamp, etc. + /// @param permissionId The specific permission ID to use for evaluating this rule. If set to `0x`, the passed permission ID will be used. + struct Rule { + uint8 id; + uint8 op; + uint240 value; + bytes32 permissionId; + } + + /// @notice Represents various operations that can be performed in a rule. + /// @param NONE No operation. + /// @param EQ Equal to operation. + /// @param NEQ Not equal to operation. + /// @param GT Greater than operation. + /// @param LT Less than operation. + /// @param GTE Greater than or equal to operation. + /// @param LTE Less than or equal to operation. + /// @param RET Return the evaluation result. + /// @param NOT Logical NOT operation. + /// @param AND Logical AND operation. + /// @param OR Logical OR operation. + /// @param XOR Logical XOR operation. + /// @param IF_ELSE Conditional evaluation with IF-ELSE logic. + enum Op { + NONE, + EQ, + NEQ, + GT, + LT, + GTE, + LTE, + RET, + NOT, + AND, + OR, + XOR, + IF_ELSE + } + + /// @notice A set of rules that will be used in the evaluation process. + Rule[] private rules; + + /// @inheritdoc PermissionConditionUpgradeable + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return + _interfaceId == type(RuledCondition).interfaceId || + super.supportsInterface(_interfaceId); + } + + /// @notice Retrieves the current rules stored in this contract. + /// @return An array of `Rule` structs representing the currently defined rules. + function getRules() public view virtual returns (Rule[] memory) { + return rules; + } + + /// @notice Updates the set of rules. + /// @dev This function deletes the current set of rules and replaces it with a new one. + /// @param _rules An new array of `Rule` structs to replace the current set of rules. + function _updateRules(Rule[] memory _rules) internal virtual { + delete rules; + + for (uint256 i; i < _rules.length; ) { + rules.push(_rules[i]); + unchecked { + ++i; + } + } + + emit RulesUpdated(_rules); + } + + /// @notice Evaluates a rule by its index. + /// @param _ruleIndex The index of the rule to evaluate. + /// @param _where The address of the target contract. + /// @param _who The address (EOA or contract) for which the permissions are checked. + /// @param _permissionId The permission identifier. + /// @param _compareList A list of values used for comparison. + /// @return Returns `true` if the rule passes. + function _evalRule( + uint32 _ruleIndex, + address _where, + address _who, + bytes32 _permissionId, + uint256[] memory _compareList + ) internal view virtual returns (bool) { + Rule memory rule = rules[_ruleIndex]; + + if (rule.id == LOGIC_OP_RULE_ID) { + return _evalLogic(rule, _where, _who, _permissionId, _compareList); + } + + uint256 value; + uint256 comparedTo = uint256(rule.value); + + // get value + if (rule.id == CONDITION_RULE_ID) { + bytes32 permissionId = rule.permissionId; + + bool conditionRes = _checkCondition( + IPermissionCondition(address(uint160(rule.value))), + _where, + _who, + permissionId == bytes32(0) ? _permissionId : permissionId, + _compareList + ); + value = conditionRes ? 1 : 0; + comparedTo = 1; + } else if (rule.id == BLOCK_NUMBER_RULE_ID) { + value = block.number; + } else if (rule.id == TIMESTAMP_RULE_ID) { + value = block.timestamp; + } else if (rule.id == VALUE_RULE_ID) { + value = uint256(rule.value); + } else { + if (rule.id >= _compareList.length) { + return false; + } + value = uint256(uint240(_compareList[rule.id])); // force lost precision + } + + if (Op(rule.op) == Op.RET) { + return uint256(value) > 0; + } + + return _compare(value, comparedTo, Op(rule.op)); + } + + /// @notice Evaluates logical operations. + /// @param _rule The rule containing the logical operation. + /// @param _where The address of the target contract. + /// @param _who The address (EOA or contract) for which the permissions are checked. + /// @param _permissionId The permission identifier. + /// @param _compareList A list of values used for comparison in evaluation. + /// @return Returns `true` if the logic evaluates to true. + function _evalLogic( + Rule memory _rule, + address _where, + address _who, + bytes32 _permissionId, + uint256[] memory _compareList + ) internal view virtual returns (bool) { + if (Op(_rule.op) == Op.IF_ELSE) { + ( + uint32 currentRuleIndex, + uint32 ruleIndexOnSuccess, + uint32 ruleIndexOnFailure + ) = decodeRuleValue(uint256(_rule.value)); + bool result = _evalRule(currentRuleIndex, _who, _where, _permissionId, _compareList); + + return + _evalRule( + result ? ruleIndexOnSuccess : ruleIndexOnFailure, + _where, + _who, + _permissionId, + _compareList + ); + } + + uint32 param1; + uint32 param2; + + (param1, param2, ) = decodeRuleValue(uint256(_rule.value)); + bool r1 = _evalRule(param1, _where, _who, _permissionId, _compareList); + + if (Op(_rule.op) == Op.NOT) { + return !r1; + } + + if (r1 && Op(_rule.op) == Op.OR) { + return true; + } + + if (!r1 && Op(_rule.op) == Op.AND) { + return false; + } + + bool r2 = _evalRule(param2, _where, _who, _permissionId, _compareList); + + if (Op(_rule.op) == Op.XOR) { + return r1 != r2; + } + + return r2; // both or and and depend on result of r2 after checks + } + + /// @notice Checks an external condition. + /// @param _condition The address of the external condition. + /// @param _where The address of the target contract. + /// @param _who The address (EOA or contract) for which the permissions are checked. + /// @param _permissionId The permission identifier. + /// @param _compareList A list of values used for comparison in evaluation. + /// @return Returns `true` if the external condition is granted. + function _checkCondition( + IPermissionCondition _condition, + address _where, + address _who, + bytes32 _permissionId, + uint256[] memory _compareList + ) internal view virtual returns (bool) { + // a raw call is required so we can return false if the call reverts, rather than reverting + bytes memory checkCalldata = abi.encodeWithSelector( + _condition.isGranted.selector, + _where, + _who, + _permissionId, + abi.encode(_compareList) + ); + + bool ok; + // solhint-disable-next-line no-inline-assembly + assembly { + // send all available gas; if the oracle eats up all the gas, we will eventually revert + // note that we are currently guaranteed to still have some gas after the call from + // EIP-150's 63/64 gas forward rule + ok := staticcall( + gas(), + _condition, + add(checkCalldata, 0x20), + mload(checkCalldata), + 0, + 0 + ) + } + + if (!ok) { + return false; + } + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := returndatasize() + } + if (size != 32) { + return false; + } + + bool result; + // solhint-disable-next-line no-inline-assembly + assembly { + let ptr := mload(0x40) // get next free memory ptr + returndatacopy(ptr, 0, size) // copy return from above `staticcall` + result := mload(ptr) // read data at ptr and set it to result + mstore(ptr, 0) // set pointer memory to 0 so it still is the next free ptr + } + + return result; + } + + /// @notice Compares two values based on the specified operation. + /// @param _a The first value to compare. + /// @param _b The second value to compare. + /// @param _op The operation to use for comparison. + /// @return Returns `true` if the comparison holds true. + function _compare(uint256 _a, uint256 _b, Op _op) internal pure returns (bool) { + if (_op == Op.EQ) return _a == _b; + if (_op == Op.NEQ) return _a != _b; + if (_op == Op.GT) return _a > _b; + if (_op == Op.LT) return _a < _b; + if (_op == Op.GTE) return _a >= _b; + if (_op == Op.LTE) return _a <= _b; + return false; + } + + /// @notice Encodes rule indices into a uint240 value. + /// @param startingRuleIndex The index of the starting rule to evaluate. + /// @param successRuleIndex The index of the rule to evaluate if the evaluation of `startingRuleIndex` was true. + /// @param failureRuleIndex The index of the rule to evaluate if the evaluation of `startingRuleIndex` was false. + /// @return The encoded value combining all three inputs. + function encodeIfElse( + uint256 startingRuleIndex, + uint256 successRuleIndex, + uint256 failureRuleIndex + ) public pure returns (uint240) { + return uint240(startingRuleIndex + (successRuleIndex << 32) + (failureRuleIndex << 64)); + } + + /// @notice Encodes two rule indexes into a uint240 value. Useful for logical operators such as `AND/OR/XOR` and others. + /// @param ruleIndex1 The first index to evaluate. + /// @param ruleIndex2 The second index to evaluate. + function encodeLogicalOperator( + uint256 ruleIndex1, + uint256 ruleIndex2 + ) public pure returns (uint240) { + return uint240(ruleIndex1 + (ruleIndex2 << 32)); + } + + /// @notice Decodes rule indices into three uint32. + /// @param _x The value to decode. + /// @return a The first 32-bit segment. + /// @return b The second 32-bit segment. + /// @return c The third 32-bit segment. + function decodeRuleValue(uint256 _x) public pure returns (uint32 a, uint32 b, uint32 c) { + a = uint32(_x); + b = uint32(_x >> (8 * 4)); + c = uint32(_x >> (8 * 8)); + } + + /// @notice This empty reserved space is put in place to allow future versions to add new variables without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). + uint256[49] private __gap; +} diff --git a/contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol b/contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol index 758431f1..58d2bc82 100644 --- a/contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol +++ b/contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol @@ -16,6 +16,7 @@ abstract contract MetadataExtensionUpgradeable is ERC165Upgradeable, DaoAuthoriz bytes32 public constant SET_METADATA_PERMISSION_ID = keccak256("SET_METADATA_PERMISSION"); // keccak256(abi.encode(uint256(keccak256("osx-commons.storage.MetadataExtension")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable-next-line const-name-snakecase bytes32 private constant MetadataExtensionStorageLocation = 0x47ff9796f72d439c6e5c30a24b9fad985a00c85a9f2258074c400a94f8746b00; @@ -31,6 +32,7 @@ abstract contract MetadataExtensionUpgradeable is ERC165Upgradeable, DaoAuthoriz pure returns (MetadataExtensionStorage storage $) { + // solhint-disable-next-line no-inline-assembly assembly { $.slot := MetadataExtensionStorageLocation } diff --git a/contracts/test/executors/executor.ts b/contracts/test/executors/executor.ts index 1fa95c05..76827ca7 100644 --- a/contracts/test/executors/executor.ts +++ b/contracts/test/executors/executor.ts @@ -3,9 +3,11 @@ import { Executor, Executor__factory, GasConsumer__factory, + IExecutor__factory, } from '../../typechain'; import {ExecutedEvent} from '../../typechain/src/executors/Executor'; -import {findEvent, flipBit} from '@aragon/osx-commons-sdk'; +import {erc165ComplianceTests} from '../helpers'; +import {findEvent, flipBit, getInterfaceId} from '@aragon/osx-commons-sdk'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {expect} from 'chai'; import {ethers} from 'hardhat'; @@ -21,7 +23,7 @@ const EventExecuted = 'Executed'; export async function getActions() { const signers = await ethers.getSigners(); const ActionExecuteFactory = new ActionExecute__factory(signers[0]); - let ActionExecute = await ActionExecuteFactory.deploy(); + const ActionExecute = await ActionExecuteFactory.deploy(); const iface = new ethers.utils.Interface(ActionExecute__factory.abi); const num = 20; @@ -62,8 +64,22 @@ describe('Executor', async () => { executor = await new Executor__factory(signers[0]).deploy(); }); + describe('ERC-165', async () => { + it('supports the `ERC-165` standard', async () => { + await erc165ComplianceTests(executor, signers[0]); + }); + + it('supports the `IExecutor` interface', async () => { + expect( + await executor.supportsInterface( + getInterfaceId(IExecutor__factory.createInterface()) + ) + ).to.be.true; + }); + }); + it('reverts if array of actions is too big', async () => { - let actions = []; + const actions = []; for (let i = 0; i < MAX_ACTIONS; i++) { actions[i] = data.succeedAction; } @@ -88,9 +104,9 @@ describe('Executor', async () => { // Allow the call to fail so we can get the error message // in the `execResults`, otherwise with allowFailureMap = 0, // it fails with `ActionFailed` even though reentrancy worked correctly. - let allowFailureMap = flipBit(0, ethers.BigNumber.from(0)); + const allowFailureMap = flipBit(0, ethers.BigNumber.from(0)); - let tx = await executor.execute( + const tx = await executor.execute( ZERO_BYTES32, [data.reentrancyAction], allowFailureMap @@ -129,7 +145,7 @@ describe('Executor', async () => { it('succeeds and correctly constructs failureMap results ', async () => { let allowFailureMap = ethers.BigNumber.from(0); - let actions = []; + const actions = []; // First 3 actions will fail actions[0] = data.failAction; @@ -148,8 +164,8 @@ describe('Executor', async () => { } // If the below call not fails, means allowFailureMap is correct. - let tx = await executor.execute(ZERO_BYTES32, actions, allowFailureMap); - let event = findEvent(await tx.wait(), EventExecuted); + const tx = await executor.execute(ZERO_BYTES32, actions, allowFailureMap); + const event = findEvent(await tx.wait(), EventExecuted); expect(event.args.actor).to.equal(ownerAddress); expect(event.args.callId).to.equal(ZERO_BYTES32); @@ -219,7 +235,7 @@ describe('Executor', async () => { // Provide too little gas so that the last `to.call` fails, but the remaining gas is enough to finish the subsequent operations. await expect( executor.execute(ZERO_BYTES32, [gasConsumingAction], allowFailureMap, { - gasLimit: expectedGas.sub(3200), + gasLimit: expectedGas.sub(32000), }) ).to.be.revertedWithCustomError(executor, 'InsufficientGas'); @@ -238,7 +254,7 @@ describe('Executor', async () => { // Prepare an action array calling `consumeGas` one times. const gasConsumingAction = { to: gasConsumer.address, - data: GasConsumer.interface.encodeFunctionData('consumeGas', [2]), + data: GasConsumer.interface.encodeFunctionData('consumeGas', [3]), value: 0, }; @@ -254,7 +270,7 @@ describe('Executor', async () => { // Provide too little gas so that the last `to.call` fails, but the remaining gas is enough to finish the subsequent operations. await expect( executor.execute(ZERO_BYTES32, [gasConsumingAction], allowFailureMap, { - gasLimit: expectedGas.sub(10000), + gasLimit: expectedGas.sub(10200), }) ).to.be.revertedWithCustomError(executor, 'InsufficientGas'); diff --git a/contracts/test/permission/condition/extensions/always-true-condition.ts b/contracts/test/permission/condition/extensions/always-true-condition.ts new file mode 100644 index 00000000..9a0abf2a --- /dev/null +++ b/contracts/test/permission/condition/extensions/always-true-condition.ts @@ -0,0 +1,42 @@ +import { + AlwaysTrueCondition, + AlwaysTrueCondition__factory, +} from '../../../../typechain'; +import {DUMMY_PERMISSION_ID} from '../../../utils/condition/condition'; +import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {expect} from 'chai'; +import {Wallet} from 'ethers'; +import {ethers} from 'hardhat'; + +describe('AlwaysTrueCondition', async () => { + it('it should always say is granted true', async () => { + const {alwaysTrueCondition} = await loadFixture(fixture); + expect( + await alwaysTrueCondition.isGranted( + Wallet.createRandom().address, + Wallet.createRandom().address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + }); +}); + +type FixtureResult = { + deployer: SignerWithAddress; + alwaysTrueCondition: AlwaysTrueCondition; +}; + +async function fixture(): Promise { + const [deployer] = await ethers.getSigners(); + + const alwaysTrueCondition = await new AlwaysTrueCondition__factory( + deployer + ).deploy(); + + return { + deployer, + alwaysTrueCondition, + }; +} diff --git a/contracts/test/permission/condition/extensions/ruled-condition.ts b/contracts/test/permission/condition/extensions/ruled-condition.ts new file mode 100644 index 00000000..187402f4 --- /dev/null +++ b/contracts/test/permission/condition/extensions/ruled-condition.ts @@ -0,0 +1,1014 @@ +import { + RuledConditionMock, + RuledConditionMock__factory, + PermissionConditionMock, + PermissionConditionMock__factory, + DAOMock, + DAOMock__factory, + IPermissionCondition__factory, +} from '../../../../typechain'; +import {RulesUpdatedEvent} from '../../../../typechain/src/permission/condition/extensions/RuledCondition'; +import {erc165ComplianceTests} from '../../../helpers'; +import { + BLOCK_NUMBER_RULE_ID, + TIMESTAMP_RULE_ID, + CONDITION_RULE_ID, + LOGIC_OP_RULE_ID, + VALUE_RULE_ID, + DUMMY_PERMISSION_ID, + Op, +} from '../../../utils/condition/condition'; +import {findEvent, getInterfaceId} from '@aragon/osx-commons-sdk'; +import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {expect} from 'chai'; +import {ethers} from 'hardhat'; + +describe('RuledCondition', async () => { + it('updates the rules and emits the event', async () => { + const {conditionMock} = await loadFixture(fixture); + + const newRules = [ + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: 777, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; + const tx = await conditionMock.updateRules(newRules); + const event = findEvent(await tx.wait(), 'RulesUpdated'); + expect(event.args.rules).to.deep.equal([ + [ + newRules[0].id, + newRules[0].op, + newRules[0].value, + newRules[0].permissionId, + ], + ]); + + const rules = await conditionMock.getRules(); + expect(rules.length).to.equal(1); + expect(rules[0].id).to.equal(202); + expect(rules[0].op).to.equal(1); + expect(rules[0].value).to.equal(777); + expect(rules[0].permissionId).to.equal(DUMMY_PERMISSION_ID); + }); + + it('should be able to eval simple rule (evaluation is true)', async () => { + const {deployer, daoMock, conditionMock, subConditionA} = await loadFixture( + fixture + ); + + await expect( + conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.reverted; + + // configure a simple rule in the condition + await conditionMock.updateRules([ + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionA.address, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + + // set answer to true in mock condition + await subConditionA.setAnswer(true); + + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + }); + + it('should be able to eval simple rule (evaluation is false)', async () => { + const {deployer, daoMock, conditionMock, subConditionA} = await loadFixture( + fixture + ); + + // configure a simple rule in the condition + await conditionMock.updateRules([ + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionA.address, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('should be able to eval complex rule (evaluation is true)', async () => { + const { + deployer, + daoMock, + conditionMock, + subConditionA, + subConditionB, + subConditionC, + } = await loadFixture(fixture); + + // configure a complex rule in the condition C || (A && B) + await conditionMock.updateRules( + C_or_B_and_A_rule( + conditionMock, + subConditionA, + subConditionB, + subConditionC + ) + ); + + // set answer to true in mock condition A and B + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(true); + + // C || (A && B) => C(false) || (A(true) && B(true)) => true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // set answer to true in mock condition C + await subConditionC.setAnswer(true); + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(false); + + // C || (A && B) => C(true) || (A(false) && B(false)) => true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + }); + + it('should be able to eval complex rule (evaluation is false)', async () => { + const { + deployer, + daoMock, + conditionMock, + subConditionA, + subConditionB, + subConditionC, + } = await loadFixture(fixture); + + // configure a complex rule in the condition C || (A && B) + await conditionMock.updateRules( + C_or_B_and_A_rule( + conditionMock, + subConditionA, + subConditionB, + subConditionC + ) + ); + + // set answer to true in mock condition A and B + await subConditionA.setAnswer(true); + + // C || (A && B) => C(false) || (A(true) && B(false)) => false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it(`evaluates 'if/else' on sub-conditions and only returns true if at least one of them returns true`, async () => { + const {deployer, daoMock, subConditionA, subConditionB, conditionMock} = + await loadFixture(fixture); + + // checks the block number is bigger or equal than 1 + await conditionMock.updateRules( + if_A_else_B(conditionMock, subConditionA, subConditionB) + ); + + // since both sub-conditions return false, our condition also returns false. + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + + // This now must return true because `if` condition is true. + await subConditionA.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // This now must return true because `else` condition is true. + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + }); + + it('should be able to eval rule that checks blockNumber', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + // checks the block number is bigger or equal than 1 + await conditionMock.updateRules([ + { + id: BLOCK_NUMBER_RULE_ID, + op: Op.GTE, + value: 1, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // checks the block number is lower than 1 + await conditionMock.updateRules([ + { + id: BLOCK_NUMBER_RULE_ID, + op: Op.LT, + value: 1, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('should be able to eval rule that checks timestamp', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + // checks the timestamp is bigger or equal than 1 + await conditionMock.updateRules([ + { + id: TIMESTAMP_RULE_ID, + op: Op.GTE, + value: 1, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // checks the timestamp is lower than 1 + await conditionMock.updateRules([ + { + id: TIMESTAMP_RULE_ID, + op: Op.LT, + value: 1, + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('evaluates AND operation', async () => { + const {deployer, daoMock, conditionMock, subConditionA, subConditionB} = + await loadFixture(fixture); + + await conditionMock.updateRules( + logic_rule(conditionMock, Op.AND, subConditionA, subConditionB) + ); + + // true AND true should return true + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // false AND true should return false + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + + // false AND false should return false + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + + // true AND false should return false + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('evaluates OR operation', async () => { + const {deployer, daoMock, conditionMock, subConditionA, subConditionB} = + await loadFixture(fixture); + + await conditionMock.updateRules( + logic_rule(conditionMock, Op.OR, subConditionA, subConditionB) + ); + + // true OR false should return true + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // false OR true should return true + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // true OR true should return true + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // false OR false should return false + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('evaluates XOR operation', async () => { + const {deployer, daoMock, conditionMock, subConditionA, subConditionB} = + await loadFixture(fixture); + + await conditionMock.updateRules( + logic_rule(conditionMock, Op.XOR, subConditionA, subConditionB) + ); + + // true XOR false should return true + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // false XOR true should return true + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + // false XOR false should return false + await subConditionA.setAnswer(false); + await subConditionB.setAnswer(false); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + + // true XOR true should return false + await subConditionA.setAnswer(true); + await subConditionB.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('evaluates NOT operation', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + await conditionMock.updateRules(not_rule(conditionMock, 0)); + + // value is 0, so NOT 0 is true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.true; + + await conditionMock.updateRules(not_rule(conditionMock, 1)); + + // value is 1, so NOT 1 is false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + '0x' + ) + ).to.be.false; + }); + + it('evaluates EQ operation', async () => { + const {deployer, daoMock, conditionMock, subConditionA} = await loadFixture( + fixture + ); + + const value = 1; + + await conditionMock.updateRules(comparison_rule(Op.EQ, value)); + + let list = [1, 2, 3]; + // 1 == 1 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [2, 2, 3]; + // 2 != 1 should return false + await subConditionA.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it('evaluates NEQ operation', async () => { + const {deployer, daoMock, conditionMock, subConditionA} = await loadFixture( + fixture + ); + + const value = 1; + + await conditionMock.updateRules(comparison_rule(Op.NEQ, value)); + + let list = [2, 2, 3]; + // 2 != 1 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [1, 2, 3]; + // 1 == 1 should return false + await subConditionA.setAnswer(true); + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it('evaluates GT operation', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const value = 5; + await conditionMock.updateRules(comparison_rule(Op.GT, value)); + + let list = [10, 20, 30]; + // 10 > 5 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [1, 2, 3]; + // 1 < 5 should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it('evaluates GTE operation', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const value = 10; + await conditionMock.updateRules(comparison_rule(Op.GTE, value)); + + let list = [10, 20, 30]; + // 10 >= 10 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [1, 2, 3]; + // 1 < 10 should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it('evaluates LT operation', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const value = 10; + await conditionMock.updateRules(comparison_rule(Op.LT, value)); + + let list = [1, 2, 3]; + // 1 < 10 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [11, 20, 33]; + // 11 > 10 should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it('evaluates LTE operation', async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const value = 10; + await conditionMock.updateRules(comparison_rule(Op.LTE, value)); + + let list = [10, 20, 30]; + // 10 <= 10 should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [11, 20, 30]; + // 11 > 10 should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it(`should return false if operation is NONE`, async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const list = [1, 2, 3]; + await conditionMock.updateRules([ + { + // compare list + id: 1, // index of the compare list + op: Op.NONE, + value: list[1], + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + + // since list is ordered should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it(`evaluates rule with compare list operation`, async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + let list = [1, 2, 3]; + await conditionMock.updateRules( + three_elements_list_ordered_rule(conditionMock, list) + ); + + // since list is ordered should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.true; + + list = [3, 2, 1]; + await conditionMock.updateRules( + three_elements_list_ordered_rule(conditionMock, list) + ); + // list is not ordered should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + + list = [2, 3, 1]; + await conditionMock.updateRules( + three_elements_list_ordered_rule(conditionMock, list) + ); + // list is not ordered should return false + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + it(`should return false if id bigger than compare list length`, async () => { + const {deployer, daoMock, conditionMock} = await loadFixture(fixture); + + const list = [1, 2, 3]; + await conditionMock.updateRules([ + { + // compare list + id: 5, // index of the compare list + op: Op.LTE, + value: list[1], + permissionId: DUMMY_PERMISSION_ID, + }, + ]); + + // since list is ordered should return true + expect( + await conditionMock.isGranted( + daoMock.address, + deployer.address, + DUMMY_PERMISSION_ID, + ethers.utils.defaultAbiCoder.encode(['uint256[]'], [list]) + ) + ).to.be.false; + }); + + describe('ERC-165', async () => { + it('supports the `ERC-165` standard', async () => { + const {deployer, conditionMock} = await loadFixture(fixture); + await erc165ComplianceTests(conditionMock, deployer); + }); + + it('supports the `IPermissionCondition` interface', async () => { + const {conditionMock} = await loadFixture(fixture); + const iface = IPermissionCondition__factory.createInterface(); + expect(await conditionMock.supportsInterface(getInterfaceId(iface))).to.be + .true; + }); + }); +}); + +type FixtureResult = { + deployer: SignerWithAddress; + daoMock: DAOMock; + conditionMock: RuledConditionMock; + subConditionA: PermissionConditionMock; + subConditionB: PermissionConditionMock; + subConditionC: PermissionConditionMock; +}; + +function if_A_else_B( + conditionMock: RuledConditionMock, + subConditionA: PermissionConditionMock, + subConditionB: PermissionConditionMock +) { + return [ + { + id: LOGIC_OP_RULE_ID, + op: Op.IF_ELSE, + value: conditionMock.encodeIfElse(1, 2, 3), + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionA.address, + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: VALUE_RULE_ID, + op: Op.RET, + value: 1, + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionB.address, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +function logic_rule( + conditionMock: RuledConditionMock, + operation: Op, + subConditionA: PermissionConditionMock, + subConditionB: PermissionConditionMock +) { + return [ + { + id: LOGIC_OP_RULE_ID, + op: operation, + value: conditionMock.encodeLogicalOperator(1, 2), + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionA.address, + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionB.address, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +function not_rule(conditionMock: RuledConditionMock, value: number) { + return [ + { + id: LOGIC_OP_RULE_ID, + op: Op.NOT, + value: conditionMock.encodeLogicalOperator(1, 2), + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: VALUE_RULE_ID, + op: Op.RET, + value: value, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +function comparison_rule(operation: Op, value: number) { + return [ + { + // compare list + id: 0, // index of the compare list + op: operation, + value: value, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +function C_or_B_and_A_rule( + conditionMock: RuledConditionMock, + subConditionA: PermissionConditionMock, + subConditionB: PermissionConditionMock, + subConditionC: PermissionConditionMock +) { + return [ + { + id: LOGIC_OP_RULE_ID, + op: Op.OR, + value: conditionMock.encodeLogicalOperator(1, 2), // indx 1 and indx 2 encoded + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionC.address, + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: LOGIC_OP_RULE_ID, + op: Op.AND, + value: conditionMock.encodeLogicalOperator(3, 4), // indx 3 and indx 4 encoded + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionA.address, + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: CONDITION_RULE_ID, + op: Op.EQ, + value: subConditionB.address, + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +function three_elements_list_ordered_rule( + conditionMock: RuledConditionMock, + list: number[] +) { + // ordered list of 3 elements + return [ + { + id: LOGIC_OP_RULE_ID, + op: Op.AND, + value: conditionMock.encodeLogicalOperator(1, 2), // indx 1 and indx 2 encoded, + permissionId: DUMMY_PERMISSION_ID, + }, + { + // compare list + id: 0, // index of the compare list + op: Op.LTE, + value: list[1], + permissionId: DUMMY_PERMISSION_ID, + }, + { + id: 1, // index of the compare list + op: Op.LTE, + value: list[2], + permissionId: DUMMY_PERMISSION_ID, + }, + ]; +} + +async function fixture(): Promise { + const [deployer] = await ethers.getSigners(); + + const daoMock = await new DAOMock__factory(deployer).deploy(); + + const conditionMock = await new RuledConditionMock__factory( + deployer + ).deploy(); + + const subConditionA = await new PermissionConditionMock__factory( + deployer + ).deploy(); + const subConditionB = await new PermissionConditionMock__factory( + deployer + ).deploy(); + const subConditionC = await new PermissionConditionMock__factory( + deployer + ).deploy(); + + return { + deployer, + daoMock, + conditionMock, + subConditionA, + subConditionB, + subConditionC, + }; +} diff --git a/contracts/test/utils/condition/condition.ts b/contracts/test/utils/condition/condition.ts new file mode 100644 index 00000000..29f5f120 --- /dev/null +++ b/contracts/test/utils/condition/condition.ts @@ -0,0 +1,25 @@ +import {ethers} from 'hardhat'; + +export const BLOCK_NUMBER_RULE_ID = 200; +export const TIMESTAMP_RULE_ID = 201; +export const CONDITION_RULE_ID = 202; +export const LOGIC_OP_RULE_ID = 203; +export const VALUE_RULE_ID = 204; + +export enum Op { + NONE, + EQ, + NEQ, + GT, + LT, + GTE, + LTE, + RET, + NOT, + AND, + OR, + XOR, + IF_ELSE, +} + +export const DUMMY_PERMISSION_ID = ethers.utils.id('DUMMY_PERMISSION');