From 334081d8b4eb69eb66a3617296a222587c15c71b Mon Sep 17 00:00:00 2001 From: Sahil Vasava Date: Fri, 9 Aug 2024 06:23:16 +0530 Subject: [PATCH] feat: created AllowedParamsEnforcer and ported over AllowedTargets and AllowedMethods enforcers from metamask repo --- src/enforcers/AllowedMethodsEnforcer.sol | 93 ++++++++++++ src/enforcers/AllowedParamsEnforcer.sol | 180 +++++++++++++++++++++++ src/enforcers/AllowedTargetsEnforcer.sol | 98 ++++++++++++ src/enforcers/CaveatEnforcerBatch.sol | 22 +++ src/interfaces/ICaveatEnforcerBatch.sol | 1 - 5 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 src/enforcers/AllowedMethodsEnforcer.sol create mode 100644 src/enforcers/AllowedParamsEnforcer.sol create mode 100644 src/enforcers/AllowedTargetsEnforcer.sol create mode 100644 src/enforcers/CaveatEnforcerBatch.sol diff --git a/src/enforcers/AllowedMethodsEnforcer.sol b/src/enforcers/AllowedMethodsEnforcer.sol new file mode 100644 index 0000000..7148cf4 --- /dev/null +++ b/src/enforcers/AllowedMethodsEnforcer.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "kernel/src/utils/ExecLib.sol"; +import {CaveatEnforcerBatch} from "./CaveatEnforcerBatch.sol"; + +/** + * @title AllowedMethodsEnforcer + * @dev This contract enforces the allowed methods a delegate may call. + */ +contract AllowedMethodsEnforcer is CaveatEnforcerBatch { + ////////////////////////////// Custom Errors ////////////////////////////// + + error InvalidCallType(); + error MethodNotAllowed(); + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice Allows the delegator to limit what methods the delegate may call. + * @dev This function enforces the allowed methods before the transaction is performed. + * @param _terms A series of 4byte method identifiers, representing the methods that the delegate is allowed to call. + * @param _executionData The executionData the delegate is trying try to execute. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + bytes32 _executionMode, + bytes calldata _executionData, + bytes32, + address, + address + ) public pure override { + bytes4[] memory allowedSignatures_ = getTermsInfo(_terms); + (CallType callType_,,,) = ExecLib.decode(ExecMode.wrap(_executionMode)); + if (callType_ == CALLTYPE_SINGLE) { + (,, bytes calldata callData_) = ExecLib.decodeSingle(_executionData); + bytes4 targetSig_ = bytes4(callData_[0:4]); + bool signaturePass_ = _checkSignature(allowedSignatures_, targetSig_); + if (!signaturePass_) { + revert MethodNotAllowed(); + } + } else if (callType_ == CALLTYPE_BATCH) { + Execution[] calldata exec_ = ExecLib.decodeBatch(_executionData); + for (uint256 j = 0; j < exec_.length; j++) { + bytes4 targetSig_ = bytes4(exec_[j].callData[0:4]); + bool signaturePass_ = _checkSignature(allowedSignatures_, targetSig_); + if (!signaturePass_) { + revert MethodNotAllowed(); + } + } + } else if (callType_ == CALLTYPE_DELEGATECALL) { + bytes4 targetSig_ = bytes4(_executionData[20:24]); + bool signaturePass_ = _checkSignature(allowedSignatures_, targetSig_); + if (!signaturePass_) { + revert MethodNotAllowed(); + } + } else { + revert InvalidCallType(); + } + } + + /** + * @dev Checks the method signature with set of allowed method signatures. + * @param _allowedSignatures The allowed signatures array. + * @param _targetSig The target method signature of the calldata. + * @return A boolean indicating whether the target method signature matches one of the allowed target method signatures. + */ + function _checkSignature(bytes4[] memory _allowedSignatures, bytes4 _targetSig) internal pure returns (bool) { + for (uint256 i = 0; i < _allowedSignatures.length; ++i) { + if (_targetSig == _allowedSignatures[i]) { + return true; + } + } + return false; + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms encoded data that is used during the execution hooks. + * @return allowedMethods_ The 4 byte identifiers for the methods that the delegate is allowed to call. + */ + function getTermsInfo(bytes calldata _terms) public pure returns (bytes4[] memory allowedMethods_) { + uint256 j = 0; + uint256 termsLength_ = _terms.length; + require(termsLength_ % 4 == 0, "AllowedMethodsEnforcer:invalid-terms-length"); + allowedMethods_ = new bytes4[](termsLength_ / 4); + for (uint256 i = 0; i < termsLength_; i += 4) { + allowedMethods_[j] = bytes4(_terms[i:i + 4]); + j++; + } + } +} diff --git a/src/enforcers/AllowedParamsEnforcer.sol b/src/enforcers/AllowedParamsEnforcer.sol new file mode 100644 index 0000000..a7d0980 --- /dev/null +++ b/src/enforcers/AllowedParamsEnforcer.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import {CaveatEnforcerBatch} from "./CaveatEnforcerBatch.sol"; +import "kernel/src/utils/ExecLib.sol"; + +struct ParamRule { + ParamCondition condition; + uint64 offset; + bytes32[] params; +} + +enum ParamCondition { + EQUAL, + GREATER_THAN, + LESS_THAN, + GREATER_THAN_OR_EQUAL, + LESS_THAN_OR_EQUAL, + NOT_EQUAL, + ONE_OF +} + +struct Permission { + CallType callType; + address target; + bytes4 selector; + ParamRule[] rules; +} + +/** + * @title AllowedParamsEnforcer + * @dev This contract enforces that target, methods and params of the calldata to be executed matches the permissions. + * @dev A common use case for this enforcer is enforcing function parameters. + */ +contract AllowedParamsEnforcer is CaveatEnforcerBatch { + ////////////////////////////// Custom Errors ////////////////////////////// + + error InvalidCallType(); + error CallViolatesParamRule(); + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice Allows the delegator to restrict the calldata that is executed + * @dev This function enforces that a subset of the calldata to be executed matches the allowed subset of calldata. + * @param _terms This is packed bytes + * @param _executionData The executionData the delegate is trying try to execute. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + bytes32 _executionMode, + bytes calldata _executionData, + bytes32, + address, + address + ) public pure override { + Permission[] memory permissions_; + + (permissions_) = getTermsInfo(_terms); + (CallType callType_,,,) = ExecLib.decode(ExecMode.wrap(_executionMode)); + if (callType_ == CALLTYPE_SINGLE) { + (address target_,, bytes calldata callData_) = ExecLib.decodeSingle(_executionData); + for (uint256 i = 0; i < permissions_.length; i++) { + Permission memory permission_ = permissions_[i]; + if ( + (permission_.target == target_ || permission_.target == address(0)) + && permission_.selector == bytes4(callData_[0:4]) && permission_.callType == CALLTYPE_SINGLE + ) { + bool permissionPass_ = _checkParams(callData_, permission_.rules); + if (!permissionPass_) { + revert CallViolatesParamRule(); + } + return; + } + } + } else if (callType_ == CALLTYPE_BATCH) { + Execution[] calldata exec_ = ExecLib.decodeBatch(_executionData); + for (uint256 j = 0; j < exec_.length; j++) { + bool permissionFoundAndPassed_ = false; + bytes4 execSelector_ = bytes4(exec_[j].callData[0:4]); + for (uint256 i = 0; i < permissions_.length; i++) { + Permission memory permission_ = permissions_[i]; + if ( + (permission_.target == exec_[j].target || permission_.target == address(0)) + && permission_.selector == execSelector_ && permission_.callType == CALLTYPE_BATCH + ) { + bool permissionPass_ = _checkParams(exec_[j].callData, permission_.rules); + if (!permissionPass_) { + revert CallViolatesParamRule(); + } + permissionFoundAndPassed_ = true; + break; + } + } + if (!permissionFoundAndPassed_) { + revert("AllowedParamsEnforcer:no-matching-permissions-found"); + } + } + return; + } else if (callType_ == CALLTYPE_DELEGATECALL) { + address target_ = address(bytes20(_executionData[0:20])); + bytes calldata callData_ = _executionData[20:]; + for (uint256 i = 0; i < permissions_.length; i++) { + Permission memory permission_ = permissions_[i]; + if ( + (permission_.target == target_ || permission_.target == address(0)) + && permission_.selector == bytes4(callData_[0:4]) && permission_.callType == CALLTYPE_DELEGATECALL + ) { + bool permissionPass_ = _checkParams(callData_, permission_.rules); + if (!permissionPass_) { + revert CallViolatesParamRule(); + } + return; + } + } + } else { + revert InvalidCallType(); + } + revert("AllowedParamsEnforcer:no-matching-permissions-found"); + } + + /** + * @dev Checks the params of the calldata to be execute with set of allowed params. + * @param _data The calldata of the execution. + * @param _rules The rules array for the params of the calldata. + * @return A boolean indicating whether all the params satisfies the defined set of rules. + */ + function _checkParams(bytes calldata _data, ParamRule[] memory _rules) internal pure returns (bool) { + for (uint256 i = 0; i < _rules.length; i++) { + ParamRule memory rule_ = _rules[i]; + bytes32 param_ = bytes32(_data[4 + rule_.offset:4 + rule_.offset + 32]); + // only ONE_OF condition can have multiple params + if (rule_.condition == ParamCondition.EQUAL && param_ != rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.GREATER_THAN && param_ <= rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.LESS_THAN && param_ >= rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.GREATER_THAN_OR_EQUAL && param_ < rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.LESS_THAN_OR_EQUAL && param_ > rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.NOT_EQUAL && param_ == rule_.params[0]) { + return false; + } else if (rule_.condition == ParamCondition.ONE_OF) { + bool oneOfStatus = false; + for (uint256 j = 0; j < rule_.params.length; j++) { + if (param_ == rule_.params[j]) { + oneOfStatus = true; + break; + } + } + if (!oneOfStatus) { + return false; + } + } + } + return true; + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms encoded data that is used during the execution hooks. + * @return permissions The permissions for the transaction. + */ + function getTermsInfo(bytes calldata _terms) public pure returns (Permission[] memory permissions) { + (permissions) = abi.decode(_terms, (Permission[])); + } + + /** + * @dev Compares two byte arrays for equality. + * @param _a The first byte array. + * @param _b The second byte array. + * @return A boolean indicating whether the byte arrays are equal. + */ + function _compare(bytes memory _a, bytes memory _b) private pure returns (bool) { + return keccak256(_a) == keccak256(_b); + } +} diff --git a/src/enforcers/AllowedTargetsEnforcer.sol b/src/enforcers/AllowedTargetsEnforcer.sol new file mode 100644 index 0000000..4b626c5 --- /dev/null +++ b/src/enforcers/AllowedTargetsEnforcer.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "kernel/src/utils/ExecLib.sol"; +import {CaveatEnforcerBatch} from "./CaveatEnforcerBatch.sol"; + +/** + * @title AllowedTargetsEnforcer + * @dev This contract enforces the allowed target addresses for a delegate. + */ +contract AllowedTargetsEnforcer is CaveatEnforcerBatch { + ////////////////////////////// Custom Errors ////////////////////////////// + + error InvalidCallType(); + error TargetNotAllowed(); + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice Allows the delegator to limit what addresses the delegate may call. + * @dev This function enforces the allowed target addresses before the transaction is performed. + * @param _terms A series of 20byte addresses, representing the addresses that the delegate is allowed to call. + * @param _executionData The executionData the delegate is trying try to execute. + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + bytes32 _executionMode, + bytes calldata _executionData, + bytes32, + address, + address + ) public pure override { + address[] memory allowedTargets_ = getTermsInfo(_terms); + (CallType callType_,,,) = ExecLib.decode(ExecMode.wrap(_executionMode)); + if (callType_ == CALLTYPE_SINGLE) { + (address targetAddress_,,) = ExecLib.decodeSingle(_executionData); + bool targetPass_ = _checkTargetAddress(allowedTargets_, targetAddress_); + if (!targetPass_) { + revert TargetNotAllowed(); + } + } else if (callType_ == CALLTYPE_BATCH) { + Execution[] calldata exec = ExecLib.decodeBatch(_executionData); + for (uint256 j = 0; j < exec.length; j++) { + address targetAddress_ = exec[j].target; + bool targetPass_ = _checkTargetAddress(allowedTargets_, targetAddress_); + if (!targetPass_) { + revert TargetNotAllowed(); + } + } + } else if (callType_ == CALLTYPE_DELEGATECALL) { + address targetAddress_ = address(bytes20(_executionData[0:20])); + bool targetPass_ = _checkTargetAddress(allowedTargets_, targetAddress_); + if (!targetPass_) { + revert TargetNotAllowed(); + } + } else { + revert InvalidCallType(); + } + + revert("AllowedTargetsEnforcer:target-address-not-allowed"); + } + + /** + * @dev Checks the target address with set of allowed target addresses. + * @param _allowedTargets The allowed targets array. + * @param _targetAddress The target address of the calldata. + * @return A boolean indicating whether the target address matches one of the allowed target addresses. + */ + function _checkTargetAddress(address[] memory _allowedTargets, address _targetAddress) + internal + pure + returns (bool) + { + for (uint256 i = 0; i < _allowedTargets.length; ++i) { + if (_targetAddress == _allowedTargets[i]) { + return true; + } + } + return false; + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms encoded data that is used during the execution hooks. + * @return allowedTargets_ The allowed target addresses. + */ + function getTermsInfo(bytes calldata _terms) public pure returns (address[] memory allowedTargets_) { + uint256 j = 0; + uint256 termsLength_ = _terms.length; + require(termsLength_ % 20 == 0, "AllowedTargetsEnforcer:invalid-terms-length"); + allowedTargets_ = new address[](termsLength_ / 20); + for (uint256 i = 0; i < termsLength_; i += 20) { + allowedTargets_[j] = address(bytes20(_terms[i:i + 20])); + j++; + } + } +} diff --git a/src/enforcers/CaveatEnforcerBatch.sol b/src/enforcers/CaveatEnforcerBatch.sol new file mode 100644 index 0000000..d0a9b64 --- /dev/null +++ b/src/enforcers/CaveatEnforcerBatch.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import {ICaveatEnforcerBatch} from "../interfaces/ICaveatEnforcerBatch.sol"; + +/** + * @title CaveatEnforcer + * @dev This abstract contract enforces caveats before and after the execution of an action. + */ +abstract contract CaveatEnforcerBatch is ICaveatEnforcerBatch { + /// @inheritdoc ICaveatEnforcerBatch + function beforeHook(bytes calldata, bytes calldata, bytes32, bytes calldata, bytes32, address, address) + public + virtual + {} + + /// @inheritdoc ICaveatEnforcerBatch + function afterHook(bytes calldata, bytes calldata, bytes32, bytes calldata, bytes32, address, address) + public + virtual + {} +} diff --git a/src/interfaces/ICaveatEnforcerBatch.sol b/src/interfaces/ICaveatEnforcerBatch.sol index 8f3f28d..e642bb5 100644 --- a/src/interfaces/ICaveatEnforcerBatch.sol +++ b/src/interfaces/ICaveatEnforcerBatch.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT AND Apache-2.0 pragma solidity 0.8.23; -import {ICaveatEnforcer, Action} from "delegation-framework/src/interfaces/ICaveatEnforcer.sol"; interface ICaveatEnforcerBatch { function beforeHook(