From 6e4dc64072b84272d7a23d8521404655aaf96a87 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 1 Apr 2025 19:34:02 +0700 Subject: [PATCH 01/11] poc --- contracts/ComposableExecutionBase.sol | 19 +++++---- contracts/ComposableExecutionLib.sol | 21 ++++++++-- contracts/ComposableExecutionModule.sol | 40 +++++++++---------- contracts/interfaces/IComposableExecution.sol | 6 +-- contracts/types/ComposabilityDataTypes.sol | 14 +++++-- test/mock/MockAccount.sol | 4 +- test/mock/MockAccountCaller.sol | 4 +- test/mock/MockAccountDelegateCaller.sol | 4 +- test/mock/MockAccountNonRevert.sol | 4 +- 9 files changed, 70 insertions(+), 46 deletions(-) diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index 230986c..fb4b367 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -4,35 +4,38 @@ pragma solidity ^0.8.27; import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; import {IComposableExecution} from "./interfaces/IComposableExecution.sol"; +import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; abstract contract ComposableExecutionBase is IComposableExecution { using ComposableExecutionLib for InputParam[]; using ComposableExecutionLib for OutputParam[]; /// @dev Override it in the account and introduce additional access control or other checks - function executeComposable(ComposableExecution[] calldata executions) external payable virtual; + function executeComposable(ComposableExecution[] calldata cExecutions) external payable virtual; /// @dev internal function to execute the composable execution flow /// First, processes the input parameters and returns the composed calldata /// Then, executes the action /// Then, processes the output parameters - function _executeComposable(ComposableExecution[] calldata executions) internal { - uint256 length = executions.length; + function _executeComposable(ComposableExecution[] calldata cExecutions) internal { + uint256 length = cExecutions.length; for (uint256 i; i < length; i++) { - ComposableExecution calldata execution = executions[i]; - bytes memory composedCalldata = execution.inputParams.processInputs(execution.functionSig); + ComposableExecution calldata cExecution = cExecutions[i]; + Execution memory execution = cExecution.inputParams.processInputs(cExecution.functionSig); bytes memory returnData; - if (execution.to != address(0)) { - returnData = _executeAction(execution.to, execution.value, composedCalldata); + if (execution.target != address(0)) { + returnData = _executeAction(execution.target, execution.value, execution.callData); } else { returnData = new bytes(0); } - execution.outputParams.processOutputs(returnData, address(this)); + cExecution.outputParams.processOutputs(returnData, address(this)); } } /// @dev Override this in the account /// using account's native execution approach + /// we do not use Execution struct as an argument to be as less opinionated as possible + /// instead we just use standard types function _executeAction(address to, uint256 value, bytes memory data) internal virtual diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index ba63232..a915ee4 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.27; import {Storage} from "./Storage.sol"; import {InputParam, OutputParam, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; +import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; // Library for composable execution handling library ComposableExecutionLib { @@ -18,14 +19,28 @@ library ComposableExecutionLib { function processInputs(InputParam[] calldata inputParams, bytes4 functionSig) internal view - returns (bytes memory) + returns (Execution memory) { + address composedTarget; + uint256 composedValue; bytes memory composedCalldata = abi.encodePacked(functionSig); uint256 length = inputParams.length; for (uint256 i; i < length; i++) { - composedCalldata = bytes.concat(composedCalldata, processInput(inputParams[i])); + if (inputParams[i].paramType == InputParamType.TARGET) { + composedTarget = abi.decode(inputParams[i].paramData, (address)); + } else if (inputParams[i].paramType == InputParamType.VALUE) { + composedValue = abi.decode(inputParams[i].paramData, (uint256)); + } else if (inputParams[i].paramType == InputParamType.CALL_DATA) { + composedCalldata = bytes.concat(composedCalldata, processInput(inputParams[i])); + } else { + revert InvalidParameterEncoding(); + } } - return composedCalldata; + return Execution({ + target: composedTarget, + value: composedValue, + callData: composedCalldata + }); } // Process a single input parameter and return the composed calldata diff --git a/contracts/ComposableExecutionModule.sol b/contracts/ComposableExecutionModule.sol index 64e2ca5..315f22c 100644 --- a/contracts/ComposableExecutionModule.sol +++ b/contracts/ComposableExecutionModule.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import {IExecutor} from "erc7579/interfaces/IERC7579Module.sol"; -import {IERC7579Account} from "erc7579/interfaces/IERC7579Account.sol"; +import {IERC7579Account, Execution} from "erc7579/interfaces/IERC7579Account.sol"; import {ModeLib} from "erc7579/lib/ModeLib.sol"; import {ExecutionLib} from "erc7579/lib/ExecutionLib.sol"; import {ERC7579FallbackBase} from "module-bases/ERC7579FallbackBase.sol"; @@ -47,7 +47,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC * @dev Returns the msg.value back to the sender (account) if any. This is done because in most cases the SA.fallback * forwards value to this module. This allows SA receiving value along with the composable execution call processed via fallback. */ - function executeComposable(ComposableExecution[] calldata executions) external payable { + function executeComposable(ComposableExecution[] calldata cExecutions) external payable { // access control address sender = _msgSender(); // in most cases, only first condition (against constant) will be checked @@ -56,60 +56,60 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC sender == entryPoints[msg.sender] || sender == msg.sender, OnlyEntryPointOrAccount()); _returnMsgValue(); - _executeComposable(executions, msg.sender, _executeExecutionCall); + _executeComposable(cExecutions, msg.sender, _executeExecutionCall); } /// @notice It doesn't require access control as it is expected to be called by the account itself via .execute() /// @dev !!! Attention !!! This function should NEVER be installed to be used via fallback() as it doesn't implement access control /// thus it will be callable by any address account.executeComposableCall => fallback() => this.executeComposableCall - function executeComposableCall(ComposableExecution[] calldata executions) external { - _executeComposable(executions, msg.sender, _executeExecutionCall); + function executeComposableCall(ComposableExecution[] calldata cExecutions) external { + _executeComposable(cExecutions, msg.sender, _executeExecutionCall); } /// @notice It doesn't require access control as it is expected to be called by the account itself via .execute(mode = delegatecall) - function executeComposableDelegateCall(ComposableExecution[] calldata executions) external { + function executeComposableDelegateCall(ComposableExecution[] calldata cExecutions) external { require(THIS_ADDRESS != address(this), DelegateCallOnly()); - _executeComposable(executions, address(this), _executeExecutionDelegatecall); + _executeComposable(cExecutions, address(this), _executeExecutionDelegatecall); } /// @dev internal function to execute the composable execution flow - /// @param executions - the composable executions to execute + /// @param cExecutions - the composable executions to execute /// @param account - the account to execute the composable executions on /// @param executeExecutionFunction - the function to execute the composable executions function _executeComposable( - ComposableExecution[] calldata executions, + ComposableExecution[] calldata cExecutions, address account, - function(ComposableExecution calldata execution, bytes memory composedCalldata) internal returns(bytes[] memory) executeExecutionFunction + function(Execution memory execution) internal returns(bytes[] memory) executeExecutionFunction ) internal { // we can not use erc-7579 batch mode here because we may need to compose // the next call in the batch based on the execution result of the previous call - uint256 length = executions.length; + uint256 length = cExecutions.length; for (uint256 i; i < length; i++) { - ComposableExecution calldata execution = executions[i]; - bytes memory composedCalldata = execution.inputParams.processInputs(execution.functionSig); + ComposableExecution calldata cExecution = cExecutions[i]; + Execution memory execution = cExecution.inputParams.processInputs(cExecution.functionSig); bytes[] memory returnData; - if (execution.to != address(0)) { - returnData = executeExecutionFunction(execution, composedCalldata); + if (execution.target != address(0)) { + returnData = executeExecutionFunction(execution); } else { returnData = new bytes[](1); returnData[0] = ""; } - execution.outputParams.processOutputs(returnData[0], account); + cExecution.outputParams.processOutputs(returnData[0], account); } } /// @dev function to be used as an argument for _executeComposable in case of regular call - function _executeExecutionCall(ComposableExecution calldata execution, bytes memory composedCalldata) internal returns (bytes[] memory) { + function _executeExecutionCall(Execution memory execution) internal returns (bytes[] memory) { return IERC7579Account(msg.sender).executeFromExecutor({ mode: ModeLib.encodeSimpleSingle(), - executionCalldata: ExecutionLib.encodeSingle(execution.to, execution.value, composedCalldata) + executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData) }); } /// @dev function to be used as an argument for _executeComposable in case of delegatecall - function _executeExecutionDelegatecall(ComposableExecution calldata execution, bytes memory composedCalldata) internal returns (bytes[] memory returnData) { + function _executeExecutionDelegatecall(Execution memory execution) internal returns (bytes[] memory returnData) { returnData = new bytes[](1); - returnData[0] = _execute(execution.to, execution.value, composedCalldata); + returnData[0] = _execute(execution.target, execution.value, execution.callData); } /// @dev sets the entry point for the account diff --git a/contracts/interfaces/IComposableExecution.sol b/contracts/interfaces/IComposableExecution.sol index fb64637..56871d9 100644 --- a/contracts/interfaces/IComposableExecution.sol +++ b/contracts/interfaces/IComposableExecution.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.23; import {ComposableExecution} from "../types/ComposabilityDataTypes.sol"; interface IComposableExecution { - function executeComposable(ComposableExecution[] calldata executions) external payable; + function executeComposable(ComposableExecution[] calldata cExecutions) external payable; } interface IComposableExecutionModule is IComposableExecution { - function executeComposableCall(ComposableExecution[] calldata executions) external; - function executeComposableDelegateCall(ComposableExecution[] calldata executions) external; + function executeComposableCall(ComposableExecution[] calldata cExecutions) external; + function executeComposableDelegateCall(ComposableExecution[] calldata cExecutions) external; } diff --git a/contracts/types/ComposabilityDataTypes.sol b/contracts/types/ComposabilityDataTypes.sol index 2337571..ef845f3 100644 --- a/contracts/types/ComposabilityDataTypes.sol +++ b/contracts/types/ComposabilityDataTypes.sol @@ -1,11 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; +// Type of the input parameter +enum InputParamType { + TARGET, // The target address + VALUE, // The value + CALL_DATA // The call data +} + // Parameter type for composition enum InputParamFetcherType { RAW_BYTES, // Already encoded bytes - STATIC_CALL // Perform a static call - + STATIC_CALL, // Perform a static call + BALANCE // Get the balance of an address } enum OutputParamFetcherType { @@ -29,6 +36,7 @@ struct Constraint { // Structure to define parameter composition struct InputParam { + InputParamType paramType; InputParamFetcherType fetcherType; // How to fetch the parameter bytes paramData; Constraint[] constraints; @@ -42,8 +50,6 @@ struct OutputParam { // Structure to define a composable execution struct ComposableExecution { - address to; - uint256 value; bytes4 functionSig; InputParam[] inputParams; OutputParam[] outputParams; diff --git a/test/mock/MockAccount.sol b/test/mock/MockAccount.sol index e6478a9..b398c44 100644 --- a/test/mock/MockAccount.sol +++ b/test/mock/MockAccount.sol @@ -66,9 +66,9 @@ contract MockAccount is ComposableExecutionBase, IAccount { (success, result) = to.call{value: value}(data); } - function executeComposable(ComposableExecution[] calldata executions) external payable override { + function executeComposable(ComposableExecution[] calldata cExecutions) external payable override { require(msg.sender == ENTRY_POINT_V07 || msg.sender == address(this), OnlyEntryPointOrSelf()); - _executeComposable(executions); + _executeComposable(cExecutions); } function _executeAction(address to, uint256 value, bytes memory data) diff --git a/test/mock/MockAccountCaller.sol b/test/mock/MockAccountCaller.sol index 24fc6ad..b26c1d1 100644 --- a/test/mock/MockAccountCaller.sol +++ b/test/mock/MockAccountCaller.sol @@ -37,8 +37,8 @@ contract MockAccountCaller is IAccount { // naming is to make testing easier. // in the wild it should be some open execution function used instead. // For example ERC-7579 `execute(mode, executionData)` - function executeComposable(ComposableExecution[] calldata executions) external payable { - IComposableExecutionModule(address(handler)).executeComposableCall(executions); + function executeComposable(ComposableExecution[] calldata cExecutions) external payable { + IComposableExecutionModule(address(handler)).executeComposableCall(cExecutions); } function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) diff --git a/test/mock/MockAccountDelegateCaller.sol b/test/mock/MockAccountDelegateCaller.sol index a141632..31d58ba 100644 --- a/test/mock/MockAccountDelegateCaller.sol +++ b/test/mock/MockAccountDelegateCaller.sol @@ -12,9 +12,9 @@ contract MockAccountDelegateCaller { composableModule = _composableModule; } - function executeComposable(ComposableExecution[] calldata executions) external payable { + function executeComposable(ComposableExecution[] calldata cExecutions) external payable { // delegatecall to the composableModule - (bool success, bytes memory returnData) = composableModule.delegatecall(abi.encodeWithSelector(IComposableExecutionModule.executeComposableDelegateCall.selector, executions)); + (bool success, bytes memory returnData) = composableModule.delegatecall(abi.encodeWithSelector(IComposableExecutionModule.executeComposableDelegateCall.selector, cExecutions)); emit MockAccountDelegateCall(returnData); assembly { if iszero(success) { diff --git a/test/mock/MockAccountNonRevert.sol b/test/mock/MockAccountNonRevert.sol index d11aa2b..cbb05d7 100644 --- a/test/mock/MockAccountNonRevert.sol +++ b/test/mock/MockAccountNonRevert.sol @@ -65,9 +65,9 @@ contract MockAccountNonRevert is ComposableExecutionBase, IAccount { (success, result) = to.call{value: value}(data); } - function executeComposable(ComposableExecution[] calldata executions) external payable override { + function executeComposable(ComposableExecution[] calldata cExecutions) external payable override { require(msg.sender == ENTRY_POINT_V07 || msg.sender == address(this), OnlyEntryPointOrSelf()); - _executeComposable(executions); + _executeComposable(cExecutions); } function _executeAction(address to, uint256 value, bytes memory data) From fad744bce5295c5e2f018e662b60a080ebc8b9a2 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 1 Apr 2025 19:39:25 +0700 Subject: [PATCH 02/11] balance fetcher --- contracts/ComposableExecutionLib.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index a915ee4..3a72024 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -65,6 +65,21 @@ library ComposableExecutionLib { } _validateConstraints(returnData, param.constraints); return returnData; + } else if (param.fetcherType == InputParamFetcherType.BALANCE) { + address contractAddr; + address account; + assembly { + contractAddr := shr(96, calldataload(paramData.offset)) + account := shr(96, calldataload(add(paramData.offset, 0x14))) + } + uint256 balance; + if (contractAddr == address(0)) { + balance = account.balance; + } else { + balance = IERC20(contractAddr).balanceOf(account); + } + _validateConstraints(abi.encode(balance), param.constraints); + return abi.encode(balance); } else { revert InvalidParameterEncoding(); } From a843a34002a05dbcfd787ee075219c3f4c9ba382 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 4 Sep 2025 20:21:48 +0300 Subject: [PATCH 03/11] feat: runtime value and target injection + fix tests --- contracts/ComposableExecutionBase.sol | 4 +- contracts/ComposableExecutionLib.sol | 22 +- contracts/types/ComposabilityDataTypes.sol | 2 +- contracts/types/Constants.sol | 2 +- test/ComposabilityBase.t.sol | 36 + test/mock/DummyContract.sol | 10 +- test/unit/ComposableExecution.t.sol | 1151 ----------------- test/unit/ComposableExecution_Complex.sol | 368 ++++++ ...ComposableExecution_ConstraintsReverts.sol | 417 ++++++ test/unit/ComposableExecution_Simple.t.sol | 484 +++++++ 10 files changed, 1333 insertions(+), 1163 deletions(-) delete mode 100644 test/unit/ComposableExecution.t.sol create mode 100644 test/unit/ComposableExecution_Complex.sol create mode 100644 test/unit/ComposableExecution_ConstraintsReverts.sol create mode 100644 test/unit/ComposableExecution_Simple.t.sol diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index fb4b367..9b5996c 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; @@ -28,6 +28,8 @@ abstract contract ComposableExecutionBase is IComposableExecution { } else { returnData = new bytes(0); } + // TODO: add early sanity check that output params length is > 0 + // so if it is 0, we can not even call processOutputs cExecution.outputParams.processOutputs(returnData, address(this)); } } diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index 3a72024..04f09c6 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; import {Storage} from "./Storage.sol"; -import {InputParam, OutputParam, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; +import {InputParam, OutputParam, Constraint, ConstraintType, InputParamType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Library for composable execution handling library ComposableExecutionLib { error ConstraintNotMet(ConstraintType constraintType); error Output_StaticCallFailed(); - error InvalidParameterEncoding(); + error InvalidParameterEncoding(string message); error InvalidOutputParamFetcherType(); error ComposableExecutionFailed(); error InvalidConstraintType(); @@ -26,14 +27,18 @@ library ComposableExecutionLib { bytes memory composedCalldata = abi.encodePacked(functionSig); uint256 length = inputParams.length; for (uint256 i; i < length; i++) { + bytes memory processedInput = processInput(inputParams[i]); if (inputParams[i].paramType == InputParamType.TARGET) { - composedTarget = abi.decode(inputParams[i].paramData, (address)); + if (inputParams[i].fetcherType == InputParamFetcherType.BALANCE) { + revert InvalidParameterEncoding("BALANCE fetcher type is not supported for TARGET param type"); + } + composedTarget = abi.decode(processedInput, (address)); } else if (inputParams[i].paramType == InputParamType.VALUE) { - composedValue = abi.decode(inputParams[i].paramData, (uint256)); + composedValue = abi.decode(processedInput, (uint256)); } else if (inputParams[i].paramType == InputParamType.CALL_DATA) { - composedCalldata = bytes.concat(composedCalldata, processInput(inputParams[i])); + composedCalldata = bytes.concat(composedCalldata, processedInput); } else { - revert InvalidParameterEncoding(); + revert InvalidParameterEncoding("Invalid param type"); } } return Execution({ @@ -68,6 +73,7 @@ library ComposableExecutionLib { } else if (param.fetcherType == InputParamFetcherType.BALANCE) { address contractAddr; address account; + bytes calldata paramData = param.paramData; assembly { contractAddr := shr(96, calldataload(paramData.offset)) account := shr(96, calldataload(add(paramData.offset, 0x14))) @@ -81,7 +87,7 @@ library ComposableExecutionLib { _validateConstraints(abi.encode(balance), param.constraints); return abi.encode(balance); } else { - revert InvalidParameterEncoding(); + revert InvalidParameterEncoding("Invalid param fetcher type"); } } diff --git a/contracts/types/ComposabilityDataTypes.sol b/contracts/types/ComposabilityDataTypes.sol index ef845f3..2004d22 100644 --- a/contracts/types/ComposabilityDataTypes.sol +++ b/contracts/types/ComposabilityDataTypes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; // Type of the input parameter enum InputParamType { diff --git a/contracts/types/Constants.sol b/contracts/types/Constants.sol index 4556c37..b849faa 100644 --- a/contracts/types/Constants.sol +++ b/contracts/types/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; bytes3 constant SIG_TYPE_MEE_FLOW = 0x177eee; diff --git a/test/ComposabilityBase.t.sol b/test/ComposabilityBase.t.sol index b34427a..e614949 100644 --- a/test/ComposabilityBase.t.sol +++ b/test/ComposabilityBase.t.sol @@ -8,6 +8,9 @@ import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol import {MockAccountDelegateCaller} from "./mock/MockAccountDelegateCaller.sol"; import {MockAccountCaller} from "./mock/MockAccountCaller.sol"; import {MockAccount} from "test/mock/MockAccount.sol"; +import {Storage} from "../contracts/Storage.sol"; +import {InputParam, Constraint, InputParamType, InputParamFetcherType} from "contracts/types/ComposabilityDataTypes.sol"; +import "./mock/DummyContract.sol"; address constant ENTRYPOINT_V07_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; @@ -19,6 +22,15 @@ contract ComposabilityTestBase is Test { MockAccountNonRevert internal mockAccountNonRevert; MockAccount internal mockAccount; + event MockAccountReceive(uint256 amount); + Storage public storageContract; + DummyContract public dummyContract; + + bytes32 public constant SLOT_A = keccak256("SLOT_A"); + bytes32 public constant SLOT_B = keccak256("SLOT_B"); + + Constraint[] internal emptyConstraints = new Constraint[](0); + function setUp() public virtual { composabilityHandler = new ComposableExecutionModule(ENTRYPOINT_V07_ADDRESS); mockAccountFallback = new MockAccountFallback({ @@ -48,5 +60,29 @@ contract ComposabilityTestBase is Test { vm.deal(address(mockAccountNonRevert), 100 ether); vm.deal(address(mockAccount), 100 ether); vm.deal(address(ENTRYPOINT_V07_ADDRESS), 100 ether); + + // Deploy contracts + storageContract = new Storage(); + dummyContract = new DummyContract(); + } + + function _createRawTargetInputParam(address target) internal returns (InputParam memory) { + return InputParam({ + paramType: InputParamType.TARGET, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(target), + constraints: emptyConstraints + }); + } + + function _createRawValueInputParam(uint256 value) internal returns (InputParam memory) { + return InputParam({ + paramType: InputParamType.VALUE, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(value), + constraints: emptyConstraints + }); } + + } diff --git a/test/mock/DummyContract.sol b/test/mock/DummyContract.sol index 9d420c7..9009a82 100644 --- a/test/mock/DummyContract.sol +++ b/test/mock/DummyContract.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; event Uint256Emitted(uint256 value); event Uint256Emitted2(uint256 value1, uint256 value2); @@ -34,6 +34,10 @@ contract DummyContract { return value * 2; } + function getNativeValue() external pure returns (uint256) { + return 10491; // 10491 wei + } + function getFoo() external view returns (uint256) { return foo; } @@ -97,4 +101,8 @@ contract DummyContract { function revertWithReason(uint256 value) external pure { revert DummyRevert(value); } + + function payableEmit() external payable { + emit Received(msg.value); + } } \ No newline at end of file diff --git a/test/unit/ComposableExecution.t.sol b/test/unit/ComposableExecution.t.sol deleted file mode 100644 index 1baf6c2..0000000 --- a/test/unit/ComposableExecution.t.sol +++ /dev/null @@ -1,1151 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import "forge-std/Test.sol"; -import "test/ComposabilityBase.t.sol"; -import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; -import {Storage} from "contracts/Storage.sol"; -import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; -import "contracts/ComposableExecutionLib.sol"; -import "test/mock/DummyContract.sol"; -import "contracts/types/ComposabilityDataTypes.sol"; - -contract ComposableExecutionTest is ComposabilityTestBase { - - event MockAccountReceive(uint256 amount); - Storage public storageContract; - DummyContract public dummyContract; - - address public eoa = address(0x11ce); - bytes32 public constant SLOT_A = keccak256("SLOT_A"); - bytes32 public constant SLOT_B = keccak256("SLOT_B"); - - Constraint[] internal emptyConstraints = new Constraint[](0); - - function setUp() public override { - super.setUp(); - // Deploy contracts - storageContract = new Storage(); - dummyContract = new DummyContract(); - // Fund EOA - vm.deal(eoa, 100 ether); - } - - function test_inputStaticCall_OutputExecResult_Success() public { - // via composability module - _inputStaticCallOutputExecResult(address(mockAccountFallback), address(composabilityHandler)); - - // via native executeComposable - _inputStaticCallOutputExecResult(address(mockAccount), address(mockAccount)); - - // via regular call - _inputStaticCallOutputExecResult(address(mockAccountCaller), address(composabilityHandler)); - - // via delegatecall - _inputStaticCallOutputExecResult(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputRawBytes_Success() public { - // via composability module - _inputRawBytes(address(mockAccountFallback), address(composabilityHandler)); - - // via native executeComposable - _inputRawBytes(address(mockAccount), address(mockAccount)); - - // via regular call - _inputRawBytes(address(mockAccountCaller), address(composabilityHandler)); - - // via delegatecall - _inputRawBytes(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_outputStaticCall_Success() public { - // via composability module - _outputStaticCall(address(mockAccountFallback), address(composabilityHandler)); - - // via native executeComposable - _outputStaticCall(address(mockAccount), address(mockAccount)); - - // via regular call - _outputStaticCall(address(mockAccountCaller), address(composabilityHandler)); - - // via delegatecall - _outputStaticCall(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - // test actual composability => call executeComposable with multiple executions - function test_useOutputAsInput_Success() public { - // via composability module - _useOutputAsInput(address(mockAccountFallback), address(composabilityHandler)); - - // via native executeComposable - _useOutputAsInput(address(mockAccount), address(mockAccount)); - - // via regular call - _useOutputAsInput(address(mockAccountCaller), address(composabilityHandler)); - - // via delegatecall - _useOutputAsInput(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_outputExecResultAddress_Success() public { - // via composability module - _outputExecResultAddress(address(mockAccountFallback), address(composabilityHandler)); - - // via native executeComposable - _outputExecResultAddress(address(mockAccount), address(mockAccount)); - - // via regular call - _outputExecResultAddress(address(mockAccountCaller), address(composabilityHandler)); - - // via delegatecall - _outputExecResultAddress(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputs_With_Gte_Constraints() public { - _inputParamUsingGteConstraints(address(mockAccount), address(mockAccount)); - _inputParamUsingGteConstraints(address(mockAccountFallback), address(composabilityHandler)); - _inputParamUsingGteConstraints(address(mockAccountCaller), address(composabilityHandler)); - _inputParamUsingGteConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputs_With_Lte_Constraints() public { - _inputParamUsingLteConstraints(address(mockAccount), address(mockAccount)); - _inputParamUsingLteConstraints(address(mockAccountFallback), address(composabilityHandler)); - _inputParamUsingLteConstraints(address(mockAccountCaller), address(composabilityHandler)); - _inputParamUsingLteConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputs_With_In_Constraints() public { - _inputParamUsingInConstraints(address(mockAccount), address(mockAccount)); - _inputParamUsingInConstraints(address(mockAccountFallback), address(composabilityHandler)); - _inputParamUsingInConstraints(address(mockAccountCaller), address(composabilityHandler)); - _inputParamUsingInConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputs_With_Eq_Constraints() public { - _inputParamUsingEqConstraints(address(mockAccount), address(mockAccount)); - _inputParamUsingEqConstraints(address(mockAccountFallback), address(composabilityHandler)); - _inputParamUsingEqConstraints(address(mockAccountCaller), address(composabilityHandler)); - _inputParamUsingEqConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_outputExecResultBool_Success() public { - _outputExecResultBool(address(mockAccountFallback), address(composabilityHandler)); - _outputExecResultBool(address(mockAccount), address(mockAccount)); - _outputExecResultBool(address(mockAccountCaller), address(composabilityHandler)); - _outputExecResultBool(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_outputExecResultMultipleValues_Success() public { - _outputExecResultMultipleValues(address(mockAccountFallback), address(composabilityHandler)); - _outputExecResultMultipleValues(address(mockAccount), address(mockAccount)); - _outputExecResultMultipleValues(address(mockAccountCaller), address(composabilityHandler)); - _outputExecResultMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_outputStaticCallMultipleValues_Success() public { - _outputStaticCallMultipleValues(address(mockAccountFallback), address(composabilityHandler)); - _outputStaticCallMultipleValues(address(mockAccount), address(mockAccount)); - _outputStaticCallMultipleValues(address(mockAccountCaller), address(composabilityHandler)); - _outputStaticCallMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputStaticCallMultipleValues_Success() public { - _inputStaticCallMultipleValues(address(mockAccountFallback), address(composabilityHandler)); - _inputStaticCallMultipleValues(address(mockAccount), address(mockAccount)); - _inputStaticCallMultipleValues(address(mockAccountCaller), address(composabilityHandler)); - _inputStaticCallMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_inputDynamicBytesArrayAsRawBytes_Success() public { - _inputDynamicBytesArrayAsRawBytes(address(mockAccountFallback), address(composabilityHandler)); - _inputDynamicBytesArrayAsRawBytes(address(mockAccount), address(mockAccount)); - _inputDynamicBytesArrayAsRawBytes(address(mockAccountCaller), address(composabilityHandler)); - _inputDynamicBytesArrayAsRawBytes(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_structInjection_Success() public { - _structInjection(address(mockAccountFallback), address(composabilityHandler)); - _structInjection(address(mockAccount), address(mockAccount)); - _structInjection(address(mockAccountCaller), address(composabilityHandler)); - _structInjection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - function test_read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized() public { - _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountFallback), address(composabilityHandler)); - _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccount), address(mockAccount)); - _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountCaller), address(composabilityHandler)); - _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } - - // if the account does not revert on unsuccessful execution, - // the revert reason is saved in the storage - function test_save_Revert_Reason_in_Storage() public { - _save_Revert_Reason_in_Storage(address(mockAccountNonRevert), address(mockAccountNonRevert)); - } - - // ================================================================================= - // ================================ TEST SCENARIOS ================================ - // ================================================================================= - - function _inputParamUsingGteConstraints(address account, address caller) internal { - Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.GTE, - referenceData: abi.encode(bytes32(uint256(43))) - }); - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - // Prepare invalid input param - call should revert - InputParam[] memory invalidInputParams = new InputParam[](1); - invalidInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(42), - constraints: constraints - }); - - // Prepare valid input param - call should succeed - InputParam[] memory validInputParams = new InputParam[](1); - validInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(43), - constraints: constraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - // Call empty function and it should revert because dynamic param value doesnt meet constraints - ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); - failingExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: invalidInputParams, // use constrainted input parameter that's going to fail - outputParams: outputParams - }); - bytes memory expectedRevertData; - if (address(account) == address(mockAccountFallback)) { - expectedRevertData = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE)); - } else { - expectedRevertData = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE); - } - vm.expectRevert(expectedRevertData); - IComposableExecution(address(account)).executeComposable(failingExecutions); - - // Call empty function and it should NOT revert because dynamic param value meets constraints - ComposableExecution[] memory validExecutions = new ComposableExecution[](1); - validExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: validInputParams, // use valid input params - outputParams: outputParams - }); - IComposableExecution(address(account)).executeComposable(validExecutions); - } - - function _inputParamUsingLteConstraints(address account, address caller) internal { - Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.LTE, - referenceData: abi.encode(bytes32(uint256(41))) - }); - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - // Prepare invalid input param - call should revert - InputParam[] memory invalidInputParams = new InputParam[](1); - invalidInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(42), - //constraints: abi.encodePacked(ConstraintType.LTE, bytes32(uint256(41))) // value must be <= 41 but 42 provided - constraints: constraints - }); - - // Prepare valid input param - call should succeed - InputParam[] memory validInputParams = new InputParam[](1); - validInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(41), - //constraints: abi.encodePacked(ConstraintType.LTE, bytes32(uint256(41))) // value must be <= 41 - constraints: constraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - // Call empty function and it should revert because dynamic param value doesnt meet constraints - ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); - failingExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: invalidInputParams, // use constrainted input parameter that's going to fail - outputParams: outputParams - }); - bytes memory expectedRevertReason; - if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE)); - } else { - expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE); - } - vm.expectRevert(expectedRevertReason); - IComposableExecution(address(account)).executeComposable(failingExecutions); - - // Call empty function and it should NOT revert because dynamic param value meets constraints - ComposableExecution[] memory validExecutions = new ComposableExecution[](1); - validExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: validInputParams, // use valid input params - outputParams: outputParams - }); - IComposableExecution(address(account)).executeComposable(validExecutions); - } - - function _inputParamUsingInConstraints(address account, address caller) internal { - Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.IN, - referenceData: abi.encode(bytes32(uint256(41)), bytes32(uint256(43))) - }); - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - // Prepare invalid input param - call should revert (param value below lowerBound) - InputParam[] memory invalidInputParamsA = new InputParam[](1); - invalidInputParamsA[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(40), - //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 - constraints: constraints - }); - - // Prepare invalid input param - call should revert (param value above upperBound) - InputParam[] memory invalidInputParamsB = new InputParam[](1); - invalidInputParamsB[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(44), - //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 - constraints: constraints - }); - - // Prepare valid input param - call should succeed (param value in bounds) - InputParam[] memory validInputParams = new InputParam[](1); - validInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(42), - //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 - constraints: constraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - // Call empty function and it should revert because dynamic param value doesnt meet constraints (value below lower bound) - ComposableExecution[] memory failingExecutionsA = new ComposableExecution[](1); - failingExecutionsA[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: invalidInputParamsA, // use constrainted input parameter that's going to fail - outputParams: outputParams - }); - bytes memory expectedRevertReason; - if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); - } else { - expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); - } - vm.expectRevert(expectedRevertReason); - IComposableExecution(address(account)).executeComposable(failingExecutionsA); - - // Call empty function and it should revert because dynamic param value doesnt meet constraints (value below lower bound) - ComposableExecution[] memory failingExecutionsB = new ComposableExecution[](1); - failingExecutionsB[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: invalidInputParamsB, // use constrainted input parameter that's going to fail - outputParams: outputParams - }); - - if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); - } else { - expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); - } - vm.expectRevert(expectedRevertReason); - IComposableExecution(address(account)).executeComposable(failingExecutionsB); - - // Call empty function and it should NOT revert because dynamic param value meets constraints - ComposableExecution[] memory validExecutions = new ComposableExecution[](1); - validExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: validInputParams, // use valid input params - outputParams: outputParams - }); - IComposableExecution(address(account)).executeComposable(validExecutions); - } - - function _inputParamUsingEqConstraints(address account, address caller) internal { - Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(42))) - }); - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - // Prepare invalid input param - call should revert - InputParam[] memory invalidInputParams = new InputParam[](1); - invalidInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(43), - //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 - constraints: constraints - }); - - // Prepare valid input param - call should succeed - InputParam[] memory validInputParams = new InputParam[](1); - validInputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(42), - //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 - constraints: constraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - // Call empty function and it should revert because dynamic param value doesnt meet constraints - ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); - failingExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: invalidInputParams, // use constrainted input parameter that's going to fail - outputParams: outputParams - }); - bytes memory expectedRevertReason; - if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ)); - } else { - expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ); - } - vm.expectRevert(expectedRevertReason); - IComposableExecution(address(account)).executeComposable(failingExecutions); - - // Call empty function and it should NOT revert because dynamic param value meets constraints - ComposableExecution[] memory validExecutions = new ComposableExecution[](1); - validExecutions[0] = ComposableExecution({ - to: address(0), // no function call - value: 0, // no value sent - functionSig: "", // no calldata encoded - inputParams: validInputParams, // use valid input params - outputParams: outputParams - }); - IComposableExecution(address(account)).executeComposable(validExecutions); - } - - function _inputStaticCallOutputExecResult(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - // Step 1: Call function A and store its result - // Prepare return value config for function A - InputParam[] memory inputParamsA = new InputParam[](0); - - OutputParam[] memory outputParamsA = new OutputParam[](1); - outputParamsA[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, storageContract, SLOT_A) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.A.selector, - inputParams: inputParamsA, // no input parameters needed - outputParams: outputParamsA // store output of the function A() to the storage - }); - - // Call function A through native executeComposable - IComposableExecution(address(account)).executeComposable(executions); - - // Verify the result (42) was stored correctly - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 storedValueA = storageContract.readStorage(namespace, SLOT_A_0); - assertEq(uint256(storedValueA), 42, "Function A result not stored correctly"); - - // Step 2: Call function B using the stored value from A - InputParam[] memory inputParamsB = new InputParam[](1); - inputParamsB[0] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A_0))), - constraints: emptyConstraints - }); - - // Prepare return value config for function B - OutputParam[] memory outputParamsB = new OutputParam[](1); - outputParamsB[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, storageContract, SLOT_B) - }); - - ComposableExecution[] memory executionsB = new ComposableExecution[](1); - executionsB[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.B.selector, - inputParams: inputParamsB, - outputParams: outputParamsB - }); - // Call function B through mockAccountFallback=>handler - IComposableExecution(address(account)).executeComposable(executionsB); - - // Verify the result (84 = 42 * 2) was stored correctly - bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); - bytes32 storedValueB = storageContract.readStorage(namespace, SLOT_B_0); - assertEq(uint256(storedValueB), 84, "Function B result not stored correctly"); - - vm.stopPrank(); - } - - // use 1 as input for emitUint256 - // so 1 should be emitted - function _inputRawBytes(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](1); - inputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(1), - constraints: emptyConstraints - }); - - // Prepare return value config for function B - OutputParam[] memory outputParams = new OutputParam[](0); - - uint256 valueToSendExecution; - uint256 valueToSendToComposableModule; - if (address(account) == address(mockAccountFallback)) { - valueToSendExecution = 1e15; - valueToSendToComposableModule = 2 * valueToSendExecution; - } - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: valueToSendExecution, - functionSig: DummyContract.emitUint256.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(1); - if (address(account) == address(mockAccountFallback)) { - vm.expectEmit(address(dummyContract)); - emit Received(valueToSendExecution); - } - IComposableExecution(address(account)).executeComposable(executions); - - vm.stopPrank(); - } - - // test static call output fetcher. - // call getFoo() on dummyContract - // store the result in the composability storage - // and check that the result is stored correctly - function _outputStaticCall(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](0); - - OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.STATIC_CALL, - paramData: abi.encode( - 1, - address(dummyContract), - abi.encodeWithSelector(DummyContract.getFoo.selector), - address(storageContract), - SLOT_B - ) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.getFoo.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - uint256 expectedValue = 2517; - dummyContract.setFoo(expectedValue); - assertEq(dummyContract.getFoo(), expectedValue, "Value not stored correctly in the contract itself"); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); - bytes32 storedValue = storageContract.readStorage(namespace, SLOT_B_0); - assertEq(uint256(storedValue), expectedValue, "Value not stored correctly in the composability storage"); - } - - function _useOutputAsInput(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - uint256 input1 = 2517; - uint256 input2 = 7579; - dummyContract.setFoo(input1); - - // first execution => call swap and store the result in the composability storage - InputParam[] memory inputParams_execution1 = new InputParam[](2); - inputParams_execution1[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(input1), - constraints: emptyConstraints - }); - inputParams_execution1[1] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(input2), - constraints: emptyConstraints - }); - - OutputParam[] memory outputParams_execution1 = new OutputParam[](2); - outputParams_execution1[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); - outputParams_execution1[1] = OutputParam({ - fetcherType: OutputParamFetcherType.STATIC_CALL, - paramData: abi.encode( - 1, - address(dummyContract), - abi.encodeWithSelector(DummyContract.getFoo.selector), - address(storageContract), - SLOT_B - ) - }); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); - // second execution => call stake with the result of the first execution - InputParam[] memory inputParams_execution2 = new InputParam[](2); - inputParams_execution2[0] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A_0))), - constraints: emptyConstraints - }); - inputParams_execution2[1] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_B_0))), - constraints: emptyConstraints - }); - OutputParam[] memory outputParams_execution2 = new OutputParam[](0); - - uint256 valueToSend = 1e15; - - ComposableExecution[] memory executions = new ComposableExecution[](2); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: valueToSend, - functionSig: DummyContract.swap.selector, - inputParams: inputParams_execution1, - outputParams: outputParams_execution1 - }); - executions[1] = ComposableExecution({ - to: address(dummyContract), - value: valueToSend, - functionSig: DummyContract.stake.selector, - inputParams: inputParams_execution2, - outputParams: outputParams_execution2 - }); - - uint256 expectedToStake = input1 + 1; - uint256 messageValue; - - if (address(account) == address(mockAccountFallback)) { - messageValue = valueToSend; - vm.expectEmit(address(mockAccountFallback)); - emit MockAccountReceive(messageValue); - } - - // swap emits input params - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted2(input1, input2); - // swap emits output param - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(expectedToStake); - // stake emits input params: first param is from swap, second param is from getFoo which is just input1 - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted2(expectedToStake, input1); - - vm.expectEmit(address(dummyContract)); - emit Received(valueToSend); - - IComposableExecution(address(account)).executeComposable{value: messageValue}(executions); - - //check storage slots - bytes32 storedValueA = storageContract.readStorage(namespace, SLOT_A_0); - assertEq(uint256(storedValueA), expectedToStake, "Value not stored correctly in the composability storage"); - bytes32 storedValueB = storageContract.readStorage(namespace, SLOT_B_0); - assertEq(uint256(storedValueB), input1, "Value not stored correctly in the composability storage"); - - vm.stopPrank(); - } - - // test that outputExecResultAddress works correctly with address - // call getAddress() on dummyContract - // store the result in the composability storage - // and check that the result is stored correctly - function _outputExecResultAddress(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](0); - - OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.getAddress.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 storedValue = storageContract.readStorage(namespace, SLOT_A_0); - assertEq(address(uint160(uint256(storedValue))), address(dummyContract), "Value not stored correctly in the composability storage"); - } - - function _outputExecResultBool(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](0); - - OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.getBool.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 storedValue = storageContract.readStorage(namespace, SLOT_A_0); - assertTrue(uint8(uint256(storedValue)) == 1, "Value not stored correctly in the composability storage"); - } - - function _outputExecResultMultipleValues(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](0); - - OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(4, address(storageContract), SLOT_A) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.returnMultipleValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 SLOT_A_1 = keccak256(abi.encodePacked(SLOT_A, uint256(1))); - bytes32 SLOT_A_2 = keccak256(abi.encodePacked(SLOT_A, uint256(2))); - bytes32 SLOT_A_3 = keccak256(abi.encodePacked(SLOT_A, uint256(3))); - bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_A_0); - bytes32 storedValue1 = storageContract.readStorage(namespace, SLOT_A_1); - bytes32 storedValue2 = storageContract.readStorage(namespace, SLOT_A_2); - bytes32 storedValue3 = storageContract.readStorage(namespace, SLOT_A_3); - assertEq(uint256(storedValue0), 2517, "Value 0 not stored correctly in the composability storage"); - assertEq(address(uint160(uint256(storedValue1))), address(dummyContract), "Value 1 not stored correctly in the composability storage"); - assertEq(storedValue2, keccak256("DUMMY"), "Value 2 not stored correctly in the composability storage"); - assertEq(uint8(uint256(storedValue3)), 1, "Value 3 not stored correctly in the composability storage"); - } - - // test outputStaticCall with multiple return values - function _outputStaticCallMultipleValues(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](0); - - OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.STATIC_CALL, - paramData: abi.encode(4, address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector), address(storageContract), SLOT_A) - }); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.A.selector, // can be any function here in fact - inputParams: inputParams, - outputParams: outputParams - }); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); - bytes32 SLOT_A_1 = keccak256(abi.encodePacked(SLOT_A, uint256(1))); - bytes32 SLOT_A_2 = keccak256(abi.encodePacked(SLOT_A, uint256(2))); - bytes32 SLOT_A_3 = keccak256(abi.encodePacked(SLOT_A, uint256(3))); - bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_A_0); - bytes32 storedValue1 = storageContract.readStorage(namespace, SLOT_A_1); - bytes32 storedValue2 = storageContract.readStorage(namespace, SLOT_A_2); - bytes32 storedValue3 = storageContract.readStorage(namespace, SLOT_A_3); - assertEq(uint256(storedValue0), 2517, "Value 0 not stored correctly in the composability storage"); - assertEq(address(uint160(uint256(storedValue1))), address(dummyContract), "Value 1 not stored correctly in the composability storage"); - assertEq(storedValue2, keccak256("DUMMY"), "Value 2 not stored correctly in the composability storage"); - assertEq(uint8(uint256(storedValue3)), 1, "Value 3 not stored correctly in the composability storage"); - } - - // test inputStaticCall with multiple return values - function _inputStaticCallMultipleValues(address account, address caller) internal { - Constraint[] memory constraints = new Constraint[](4); - constraints[0] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(2517))) - }); - constraints[1] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(uint160(address(dummyContract))))) - }); - constraints[2] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(keccak256("DUMMY")))) - }); - constraints[3] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(1))) - }); - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParams = new InputParam[](1); - inputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector)), - constraints: constraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.acceptMultipleValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(2517); - vm.expectEmit(address(dummyContract)); - emit AddressEmitted(address(dummyContract)); - vm.expectEmit(address(dummyContract)); - emit Bytes32Emitted(keccak256("DUMMY")); - vm.expectEmit(address(dummyContract)); - emit BoolEmitted(true); - - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - } - - function _inputDynamicBytesArrayAsRawBytes(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - uint256 someStaticValue = 2517; - uint256 expectedUint256 = 2517*2; - bytes memory expectedBytes = bytes("Hello, world!"); - address expectedAddress = address(0xa11cedecaf); - - // encode function call as per https://docs.soliditylang.org/en/develop/abi-spec.html - // function is : function acceptStaticAndDynamicValues(uint256 staticValue, bytes calldata dynamicValue, address addr) - - // static arg - InputParam[] memory inputParams = new InputParam[](4); - inputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.B.selector, someStaticValue)), - constraints: emptyConstraints - }); - - // dynamic arg => here only offset is pasted - inputParams[1] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(uint256(0x60)), - constraints: emptyConstraints - }); - - // static arg - inputParams[2] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(expectedAddress), - constraints: emptyConstraints - }); - - // the payload of the dynamic arg - inputParams[3] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encodePacked(expectedBytes.length, expectedBytes), - constraints: emptyConstraints - }); - - // Prepare return value config for function B - OutputParam[] memory outputParams = new OutputParam[](0); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.acceptStaticAndDynamicValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(expectedUint256); - vm.expectEmit(address(dummyContract)); - emit AddressEmitted(expectedAddress); - vm.expectEmit(address(dummyContract)); - emit BytesEmitted(expectedBytes); - IComposableExecution(address(account)).executeComposable(executions); - - vm.stopPrank(); - } - - function _structInjection(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - uint256 someStaticValue = 2517; - - address tokenIn = address(0xa11ce70c3170); - address tokenOut = address(0xb0b70c3170); - uint256 amountOutMin = 999; - uint256 deadline = block.timestamp + 1000; - uint256 fee = 500; - - Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.LTE, - referenceData: abi.encode(bytes32(uint256(10_000))) - }); - - // represent the encoded call to acceptStruct() - // as per abi encoding rules - InputParam[] memory inputParams = new InputParam[](7); - - // static param - inputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(someStaticValue), - constraints: emptyConstraints - }); - - // === start struct == - - // tokenIn - inputParams[1] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(tokenIn), - constraints: emptyConstraints - }); - - // tokenOut - inputParams[2] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(tokenOut), - constraints: emptyConstraints - }); - - // amountIn - inputParams[3] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.B.selector, someStaticValue)), - constraints: constraints - }); - - // amountOutMin - inputParams[4] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(amountOutMin), - constraints: emptyConstraints - }); - - - // deadline - inputParams[5] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(deadline), - constraints: emptyConstraints - }); - - // fee - inputParams[6] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(fee), - constraints: emptyConstraints - }); - - // === end struct == - - OutputParam[] memory outputParams = new OutputParam[](0); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.acceptStruct.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(someStaticValue); // someValue - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(someStaticValue*2); //amountIn - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(amountOutMin); //amountOutMin - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(deadline); - vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(fee); - vm.expectEmit(address(dummyContract)); - emit AddressEmitted(tokenIn); - vm.expectEmit(address(dummyContract)); - emit AddressEmitted(tokenOut); - IComposableExecution(address(account)).executeComposable(executions); - - vm.stopPrank(); - } - - // It can happen when the previous call, that creates the output params, fail. - // In this case, the composable execution should revert when reading this from storage - function _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - - assertFalse(storageContract.isSlotInitialized(namespace, SLOT_A), "Slot should not be initialized"); - - InputParam[] memory inputParams = new InputParam[](1); - inputParams[0] = InputParam({ - fetcherType: InputParamFetcherType.STATIC_CALL, - paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A))), - constraints: emptyConstraints - }); - - OutputParam[] memory outputParams = new OutputParam[](0); - - ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.B.selector, - inputParams: inputParams, - outputParams: outputParams - }); - - bytes memory expectedRevertReason; - if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector)); - } else { - expectedRevertReason = abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector); - } - vm.expectRevert(expectedRevertReason); - IComposableExecution(address(account)).executeComposable(executions); - vm.stopPrank(); - } - - // use some account that does not revert when one of the execution fails - // and saves the revert reason in the storage - function _save_Revert_Reason_in_Storage(address account, address caller) internal { - uint256 someStaticValue = 2517; - - vm.startPrank(ENTRYPOINT_V07_ADDRESS); - - InputParam[] memory inputParamsExecA = new InputParam[](1); - inputParamsExecA[0] = InputParam({ - fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(someStaticValue), - constraints: emptyConstraints - }); - - OutputParam[] memory outputParamsExecA = new OutputParam[](1); - outputParamsExecA[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode( - 1, - address(storageContract), - SLOT_B - ) - }); - - ComposableExecution[] memory executionsA = new ComposableExecution[](1); - executionsA[0] = ComposableExecution({ - to: address(dummyContract), - value: 0, // no value sent - functionSig: DummyContract.revertWithReason.selector, - inputParams: inputParamsExecA, - outputParams: outputParamsExecA - }); - - IComposableExecution(address(account)).executeComposable(executionsA); - - bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); - bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); - bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_B_0); - - bytes32 expectedValue = bytes32(DummyRevert.selector); - assertEq(storedValue0, expectedValue, "Value 0 not stored correctly in the composability storage"); - - vm.stopPrank(); - } - -} diff --git a/test/unit/ComposableExecution_Complex.sol b/test/unit/ComposableExecution_Complex.sol new file mode 100644 index 0000000..561c647 --- /dev/null +++ b/test/unit/ComposableExecution_Complex.sol @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "test/ComposabilityBase.t.sol"; +import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; +import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import "contracts/ComposableExecutionLib.sol"; +import "contracts/types/ComposabilityDataTypes.sol"; + +contract ComposableExecutionTestComplexCases is ComposabilityTestBase { + + function setUp() public override { + super.setUp(); + } + + function test_outputExecResultMultipleValues_Success() public { + _outputExecResultMultipleValues(address(mockAccountFallback), address(composabilityHandler)); + _outputExecResultMultipleValues(address(mockAccount), address(mockAccount)); + _outputExecResultMultipleValues(address(mockAccountCaller), address(composabilityHandler)); + _outputExecResultMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_outputStaticCallMultipleValues_Success() public { + _outputStaticCallMultipleValues(address(mockAccountFallback), address(composabilityHandler)); + _outputStaticCallMultipleValues(address(mockAccount), address(mockAccount)); + _outputStaticCallMultipleValues(address(mockAccountCaller), address(composabilityHandler)); + _outputStaticCallMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputStaticCallMultipleValues_Success() public { + _inputStaticCallMultipleValues(address(mockAccountFallback), address(composabilityHandler)); + _inputStaticCallMultipleValues(address(mockAccount), address(mockAccount)); + _inputStaticCallMultipleValues(address(mockAccountCaller), address(composabilityHandler)); + _inputStaticCallMultipleValues(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputDynamicBytesArrayAsRawBytes_Success() public { + _inputDynamicBytesArrayAsRawBytes(address(mockAccountFallback), address(composabilityHandler)); + _inputDynamicBytesArrayAsRawBytes(address(mockAccount), address(mockAccount)); + _inputDynamicBytesArrayAsRawBytes(address(mockAccountCaller), address(composabilityHandler)); + _inputDynamicBytesArrayAsRawBytes(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_structInjection_Success() public { + _structInjection(address(mockAccountFallback), address(composabilityHandler)); + _structInjection(address(mockAccount), address(mockAccount)); + _structInjection(address(mockAccountCaller), address(composabilityHandler)); + _structInjection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + // ================================================================================= + // ================================ TEST SCENARIOS ================================ + // ================================================================================= + + function _outputExecResultMultipleValues(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](1); + outputParams[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(4, address(storageContract), SLOT_A) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.returnMultipleValues.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 SLOT_A_1 = keccak256(abi.encodePacked(SLOT_A, uint256(1))); + bytes32 SLOT_A_2 = keccak256(abi.encodePacked(SLOT_A, uint256(2))); + bytes32 SLOT_A_3 = keccak256(abi.encodePacked(SLOT_A, uint256(3))); + bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_A_0); + bytes32 storedValue1 = storageContract.readStorage(namespace, SLOT_A_1); + bytes32 storedValue2 = storageContract.readStorage(namespace, SLOT_A_2); + bytes32 storedValue3 = storageContract.readStorage(namespace, SLOT_A_3); + assertEq(uint256(storedValue0), 2517, "Value 0 not stored correctly in the composability storage"); + assertEq(address(uint160(uint256(storedValue1))), address(dummyContract), "Value 1 not stored correctly in the composability storage"); + assertEq(storedValue2, keccak256("DUMMY"), "Value 2 not stored correctly in the composability storage"); + assertEq(uint8(uint256(storedValue3)), 1, "Value 3 not stored correctly in the composability storage"); + } + + // test outputStaticCall with multiple return values + function _outputStaticCallMultipleValues(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](1); + outputParams[0] = OutputParam({ + fetcherType: OutputParamFetcherType.STATIC_CALL, + paramData: abi.encode(4, address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector), address(storageContract), SLOT_A) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.A.selector, // can be any function here in fact + inputParams: inputParams, + outputParams: outputParams + }); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 SLOT_A_1 = keccak256(abi.encodePacked(SLOT_A, uint256(1))); + bytes32 SLOT_A_2 = keccak256(abi.encodePacked(SLOT_A, uint256(2))); + bytes32 SLOT_A_3 = keccak256(abi.encodePacked(SLOT_A, uint256(3))); + bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_A_0); + bytes32 storedValue1 = storageContract.readStorage(namespace, SLOT_A_1); + bytes32 storedValue2 = storageContract.readStorage(namespace, SLOT_A_2); + bytes32 storedValue3 = storageContract.readStorage(namespace, SLOT_A_3); + assertEq(uint256(storedValue0), 2517, "Value 0 not stored correctly in the composability storage"); + assertEq(address(uint160(uint256(storedValue1))), address(dummyContract), "Value 1 not stored correctly in the composability storage"); + assertEq(storedValue2, keccak256("DUMMY"), "Value 2 not stored correctly in the composability storage"); + assertEq(uint8(uint256(storedValue3)), 1, "Value 3 not stored correctly in the composability storage"); + } + + // test inputStaticCall with multiple return values + function _inputStaticCallMultipleValues(address account, address caller) internal { + Constraint[] memory constraints = new Constraint[](4); + constraints[0] = Constraint({ + constraintType: ConstraintType.EQ, + referenceData: abi.encode(bytes32(uint256(2517))) + }); + constraints[1] = Constraint({ + constraintType: ConstraintType.EQ, + referenceData: abi.encode(bytes32(uint256(uint160(address(dummyContract))))) + }); + constraints[2] = Constraint({ + constraintType: ConstraintType.EQ, + referenceData: abi.encode(bytes32(uint256(keccak256("DUMMY")))) + }); + constraints[3] = Constraint({ + constraintType: ConstraintType.EQ, + referenceData: abi.encode(bytes32(uint256(1))) + }); + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](3); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector)), + constraints: constraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.acceptMultipleValues.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(2517); + vm.expectEmit(address(dummyContract)); + emit AddressEmitted(address(dummyContract)); + vm.expectEmit(address(dummyContract)); + emit Bytes32Emitted(keccak256("DUMMY")); + vm.expectEmit(address(dummyContract)); + emit BoolEmitted(true); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + } + + function _inputDynamicBytesArrayAsRawBytes(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 someStaticValue = 2517; + uint256 expectedUint256 = 2517*2; + bytes memory expectedBytes = bytes("Hello, world!"); + address expectedAddress = address(0xa11cedecaf); + + // encode function call as per https://docs.soliditylang.org/en/develop/abi-spec.html + // function is : function acceptStaticAndDynamicValues(uint256 staticValue, bytes calldata dynamicValue, address addr) + + // static arg + InputParam[] memory inputParams = new InputParam[](6); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.B.selector, someStaticValue)), + constraints: emptyConstraints + }); + + // dynamic arg => here only offset is pasted + inputParams[3] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(uint256(0x60)), + constraints: emptyConstraints + }); + + // static arg + inputParams[4] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(expectedAddress), + constraints: emptyConstraints + }); + + // the payload of the dynamic arg + inputParams[5] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encodePacked(expectedBytes.length, expectedBytes), + constraints: emptyConstraints + }); + + // Prepare return value config for function B + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.acceptStaticAndDynamicValues.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(expectedUint256); + vm.expectEmit(address(dummyContract)); + emit AddressEmitted(expectedAddress); + vm.expectEmit(address(dummyContract)); + emit BytesEmitted(expectedBytes); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } + + function _structInjection(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 someStaticValue = 2517; + + address tokenIn = address(0xa11ce70c3170); + address tokenOut = address(0xb0b70c3170); + uint256 amountOutMin = 999; + uint256 deadline = block.timestamp + 1000; + uint256 fee = 500; + + Constraint[] memory constraints = new Constraint[](1); + constraints[0] = Constraint({ + constraintType: ConstraintType.LTE, + referenceData: abi.encode(bytes32(uint256(10_000))) + }); + + // represent the encoded call to acceptStruct() + // as per abi encoding rules + InputParam[] memory inputParams = new InputParam[](9); + + // TARGET and VALUE parameters + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + // static param + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(someStaticValue), + constraints: emptyConstraints + }); + + // === start struct == + + // tokenIn + inputParams[3] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(tokenIn), + constraints: emptyConstraints + }); + + // tokenOut + inputParams[4] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(tokenOut), + constraints: emptyConstraints + }); + + // amountIn + inputParams[5] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.B.selector, someStaticValue)), + constraints: constraints + }); + + // amountOutMin + inputParams[6] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(amountOutMin), + constraints: emptyConstraints + }); + + + // deadline + inputParams[7] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(deadline), + constraints: emptyConstraints + }); + + // fee + inputParams[8] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(fee), + constraints: emptyConstraints + }); + + // === end struct == + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.acceptStruct.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(someStaticValue); // someValue + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(someStaticValue*2); //amountIn + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(amountOutMin); //amountOutMin + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(deadline); + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(fee); + vm.expectEmit(address(dummyContract)); + emit AddressEmitted(tokenIn); + vm.expectEmit(address(dummyContract)); + emit AddressEmitted(tokenOut); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } +} + diff --git a/test/unit/ComposableExecution_ConstraintsReverts.sol b/test/unit/ComposableExecution_ConstraintsReverts.sol new file mode 100644 index 0000000..0ed2d24 --- /dev/null +++ b/test/unit/ComposableExecution_ConstraintsReverts.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "test/ComposabilityBase.t.sol"; +import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; +import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import "contracts/ComposableExecutionLib.sol"; +import "contracts/types/ComposabilityDataTypes.sol"; + +contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { + + function setUp() public override { + super.setUp(); + } + + function test_inputs_With_Gte_Constraints() public { + _inputParamUsingGteConstraints(address(mockAccount), address(mockAccount)); + _inputParamUsingGteConstraints(address(mockAccountFallback), address(composabilityHandler)); + _inputParamUsingGteConstraints(address(mockAccountCaller), address(composabilityHandler)); + _inputParamUsingGteConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputs_With_Lte_Constraints() public { + _inputParamUsingLteConstraints(address(mockAccount), address(mockAccount)); + _inputParamUsingLteConstraints(address(mockAccountFallback), address(composabilityHandler)); + _inputParamUsingLteConstraints(address(mockAccountCaller), address(composabilityHandler)); + _inputParamUsingLteConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputs_With_In_Constraints() public { + _inputParamUsingInConstraints(address(mockAccount), address(mockAccount)); + _inputParamUsingInConstraints(address(mockAccountFallback), address(composabilityHandler)); + _inputParamUsingInConstraints(address(mockAccountCaller), address(composabilityHandler)); + _inputParamUsingInConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputs_With_Eq_Constraints() public { + _inputParamUsingEqConstraints(address(mockAccount), address(mockAccount)); + _inputParamUsingEqConstraints(address(mockAccountFallback), address(composabilityHandler)); + _inputParamUsingEqConstraints(address(mockAccountCaller), address(composabilityHandler)); + _inputParamUsingEqConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized() public { + _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountFallback), address(composabilityHandler)); + _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccount), address(mockAccount)); + _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountCaller), address(composabilityHandler)); + _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + // if the account does not revert on unsuccessful execution, + // the revert reason is saved in the storage + function test_save_Revert_Reason_in_Storage() public { + _save_Revert_Reason_in_Storage(address(mockAccountNonRevert), address(mockAccountNonRevert)); + } + + // ================================================================================= + // ================================ TEST SCENARIOS ================================ + // ================================================================================= + + function _inputParamUsingGteConstraints(address account, address caller) internal { + Constraint[] memory constraints = new Constraint[](1); + constraints[0] = Constraint({ + constraintType: ConstraintType.GTE, + referenceData: abi.encode(bytes32(uint256(43))) + }); + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + // Prepare invalid input param - call should revert + InputParam[] memory invalidInputParams = new InputParam[](3); + invalidInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(42), + constraints: constraints + }); + invalidInputParams[1] = _createRawTargetInputParam(address(0)); + invalidInputParams[2] = _createRawValueInputParam(0); + + // Prepare valid input param - call should succeed + InputParam[] memory validInputParams = new InputParam[](3); + validInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(43), + constraints: constraints + }); + validInputParams[1] = _createRawTargetInputParam(address(0)); + validInputParams[2] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](0); + + // Call empty function and it should revert because dynamic param value doesnt meet constraints + ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); + failingExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: invalidInputParams, // use constrainted input parameter that's going to fail + outputParams: outputParams + }); + bytes memory expectedRevertData; + if (address(account) == address(mockAccountFallback)) { + expectedRevertData = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE)); + } else { + expectedRevertData = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE); + } + vm.expectRevert(expectedRevertData); + IComposableExecution(address(account)).executeComposable(failingExecutions); + + // Call empty function and it should NOT revert because dynamic param value meets constraints + ComposableExecution[] memory validExecutions = new ComposableExecution[](1); + validExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: validInputParams, // use valid input params + outputParams: outputParams + }); + IComposableExecution(address(account)).executeComposable(validExecutions); + } + + function _inputParamUsingLteConstraints(address account, address caller) internal { + Constraint[] memory constraints = new Constraint[](1); + constraints[0] = Constraint({ + constraintType: ConstraintType.LTE, + referenceData: abi.encode(bytes32(uint256(41))) + }); + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + // Prepare invalid input param - call should revert + InputParam[] memory invalidInputParams = new InputParam[](3); + invalidInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(42), + //constraints: abi.encodePacked(ConstraintType.LTE, bytes32(uint256(41))) // value must be <= 41 but 42 provided + constraints: constraints + }); + invalidInputParams[1] = _createRawTargetInputParam(address(0)); + invalidInputParams[2] = _createRawValueInputParam(0); + + // Prepare valid input param - call should succeed + InputParam[] memory validInputParams = new InputParam[](3); + validInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(41), + //constraints: abi.encodePacked(ConstraintType.LTE, bytes32(uint256(41))) // value must be <= 41 + constraints: constraints + }); + validInputParams[1] = _createRawTargetInputParam(address(0)); + validInputParams[2] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](0); + + // Call empty function and it should revert because dynamic param value doesnt meet constraints + ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); + failingExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: invalidInputParams, // use constrainted input parameter that's going to fail + outputParams: outputParams + }); + bytes memory expectedRevertReason; + if (address(account) == address(mockAccountFallback)) { + expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE)); + } else { + expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE); + } + vm.expectRevert(expectedRevertReason); + IComposableExecution(address(account)).executeComposable(failingExecutions); + + // Call empty function and it should NOT revert because dynamic param value meets constraints + ComposableExecution[] memory validExecutions = new ComposableExecution[](1); + validExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: validInputParams, // use valid input params + outputParams: outputParams + }); + IComposableExecution(address(account)).executeComposable(validExecutions); + } + + function _inputParamUsingInConstraints(address account, address caller) internal { + Constraint[] memory constraints = new Constraint[](1); + constraints[0] = Constraint({ + constraintType: ConstraintType.IN, + referenceData: abi.encode(bytes32(uint256(41)), bytes32(uint256(43))) + }); + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + // Prepare invalid input param - call should revert (param value below lowerBound) + InputParam[] memory invalidInputParamsA = new InputParam[](3); + invalidInputParamsA[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(40), + //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 + constraints: constraints + }); + invalidInputParamsA[1] = _createRawTargetInputParam(address(0)); + invalidInputParamsA[2] = _createRawValueInputParam(0); + + // Prepare invalid input param - call should revert (param value above upperBound) + InputParam[] memory invalidInputParamsB = new InputParam[](3); + invalidInputParamsB[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(44), + //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 + constraints: constraints + }); + invalidInputParamsB[1] = _createRawTargetInputParam(address(0)); + invalidInputParamsB[2] = _createRawValueInputParam(0); + + // Prepare valid input param - call should succeed (param value in bounds) + InputParam[] memory validInputParams = new InputParam[](3); + validInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(42), + //constraints: abi.encodePacked(ConstraintType.IN, abi.encode(bytes32(uint256(41)), bytes32(uint256(43)))) // value must be between 41 & 43 + constraints: constraints + }); + validInputParams[1] = _createRawTargetInputParam(address(0)); + validInputParams[2] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](0); + + // Call empty function and it should revert because dynamic param value doesnt meet constraints (value below lower bound) + ComposableExecution[] memory failingExecutionsA = new ComposableExecution[](1); + failingExecutionsA[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: invalidInputParamsA, // use constrainted input parameter that's going to fail + outputParams: outputParams + }); + bytes memory expectedRevertReason; + if (address(account) == address(mockAccountFallback)) { + expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); + } else { + expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); + } + vm.expectRevert(expectedRevertReason); + IComposableExecution(address(account)).executeComposable(failingExecutionsA); + + // Call empty function and it should revert because dynamic param value doesnt meet constraints (value below lower bound) + ComposableExecution[] memory failingExecutionsB = new ComposableExecution[](1); + failingExecutionsB[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: invalidInputParamsB, // use constrainted input parameter that's going to fail + outputParams: outputParams + }); + + if (address(account) == address(mockAccountFallback)) { + expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); + } else { + expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); + } + vm.expectRevert(expectedRevertReason); + IComposableExecution(address(account)).executeComposable(failingExecutionsB); + + // Call empty function and it should NOT revert because dynamic param value meets constraints + ComposableExecution[] memory validExecutions = new ComposableExecution[](1); + validExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: validInputParams, // use valid input params + outputParams: outputParams + }); + IComposableExecution(address(account)).executeComposable(validExecutions); + } + + function _inputParamUsingEqConstraints(address account, address caller) internal { + Constraint[] memory constraints = new Constraint[](1); + constraints[0] = Constraint({ + constraintType: ConstraintType.EQ, + referenceData: abi.encode(bytes32(uint256(42))) + }); + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + // Prepare invalid input param - call should revert + InputParam[] memory invalidInputParams = new InputParam[](3); + invalidInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(43), + //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 + constraints: constraints + }); + invalidInputParams[1] = _createRawTargetInputParam(address(0)); + invalidInputParams[2] = _createRawValueInputParam(0); + + // Prepare valid input param - call should succeed + InputParam[] memory validInputParams = new InputParam[](3); + validInputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(42), + //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 + constraints: constraints + }); + validInputParams[1] = _createRawTargetInputParam(address(0)); + validInputParams[2] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](0); + + // Call empty function and it should revert because dynamic param value doesnt meet constraints + ComposableExecution[] memory failingExecutions = new ComposableExecution[](1); + failingExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: invalidInputParams, // use constrainted input parameter that's going to fail + outputParams: outputParams + }); + bytes memory expectedRevertReason; + if (address(account) == address(mockAccountFallback)) { + expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ)); + } else { + expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ); + } + vm.expectRevert(expectedRevertReason); + IComposableExecution(address(account)).executeComposable(failingExecutions); + + // Call empty function and it should NOT revert because dynamic param value meets constraints + ComposableExecution[] memory validExecutions = new ComposableExecution[](1); + validExecutions[0] = ComposableExecution({ + functionSig: "", // no calldata encoded + inputParams: validInputParams, // use valid input params + outputParams: outputParams + }); + IComposableExecution(address(account)).executeComposable(validExecutions); + } + + // It can happen when the previous call, that creates the output params, fail. + // In this case, the composable execution should revert when reading this from storage + function _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + + assertFalse(storageContract.isSlotInitialized(namespace, SLOT_A), "Slot should not be initialized"); + + InputParam[] memory inputParams = new InputParam[](3); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A))), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.B.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + bytes memory expectedRevertReason; + if (address(account) == address(mockAccountFallback)) { + expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector)); + } else { + expectedRevertReason = abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector); + } + vm.expectRevert(expectedRevertReason); + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + } + + // use some account that does not revert when one of the execution fails + // and saves the revert reason in the storage + function _save_Revert_Reason_in_Storage(address account, address caller) internal { + uint256 someStaticValue = 2517; + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParamsExecA = new InputParam[](3); + inputParamsExecA[0] = _createRawTargetInputParam(address(dummyContract)); + inputParamsExecA[1] = _createRawValueInputParam(0); + inputParamsExecA[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(someStaticValue), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParamsExecA = new OutputParam[](1); + outputParamsExecA[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode( + 1, + address(storageContract), + SLOT_B + ) + }); + + ComposableExecution[] memory executionsA = new ComposableExecution[](1); + executionsA[0] = ComposableExecution({ + functionSig: DummyContract.revertWithReason.selector, + inputParams: inputParamsExecA, + outputParams: outputParamsExecA + }); + + IComposableExecution(address(account)).executeComposable(executionsA); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); + bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_B_0); + + bytes32 expectedValue = bytes32(DummyRevert.selector); + assertEq(storedValue0, expectedValue, "Value 0 not stored correctly in the composability storage"); + + vm.stopPrank(); + } +} + diff --git a/test/unit/ComposableExecution_Simple.t.sol b/test/unit/ComposableExecution_Simple.t.sol new file mode 100644 index 0000000..5f80224 --- /dev/null +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "test/ComposabilityBase.t.sol"; +import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; +import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import "contracts/ComposableExecutionLib.sol"; +import "contracts/types/ComposabilityDataTypes.sol"; + +contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { + + function setUp() public override { + super.setUp(); + } + + function test_inputStaticCall_OutputExecResult_Success() public { + // via composability module + _inputStaticCallOutputExecResult(address(mockAccountFallback), address(composabilityHandler)); + + // via native executeComposable + _inputStaticCallOutputExecResult(address(mockAccount), address(mockAccount)); + + // via regular call + _inputStaticCallOutputExecResult(address(mockAccountCaller), address(composabilityHandler)); + + // via delegatecall + _inputStaticCallOutputExecResult(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_inputRawBytes_Success() public { + _inputRawBytes(address(mockAccountFallback), address(composabilityHandler)); + _inputRawBytes(address(mockAccount), address(mockAccount)); + _inputRawBytes(address(mockAccountCaller), address(composabilityHandler)); + _inputRawBytes(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_outputStaticCall_Success() public { + _outputStaticCall(address(mockAccountFallback), address(composabilityHandler)); + _outputStaticCall(address(mockAccount), address(mockAccount)); + _outputStaticCall(address(mockAccountCaller), address(composabilityHandler)); + _outputStaticCall(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + // test actual composability => call executeComposable with multiple executions + function test_useOutputAsInput_Success() public { + _useOutputAsInput(address(mockAccountFallback), address(composabilityHandler)); + _useOutputAsInput(address(mockAccount), address(mockAccount)); + _useOutputAsInput(address(mockAccountCaller), address(composabilityHandler)); + _useOutputAsInput(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_outputExecResultAddress_Success() public { + _outputExecResultAddress(address(mockAccountFallback), address(composabilityHandler)); + _outputExecResultAddress(address(mockAccount), address(mockAccount)); + _outputExecResultAddress(address(mockAccountCaller), address(composabilityHandler)); + _outputExecResultAddress(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_outputExecResultBool_Success() public { + _outputExecResultBool(address(mockAccountFallback), address(composabilityHandler)); + _outputExecResultBool(address(mockAccount), address(mockAccount)); + _outputExecResultBool(address(mockAccountCaller), address(composabilityHandler)); + _outputExecResultBool(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_Runtime_Value_Injection_Success() public { + _runtime_Value_Injection(address(mockAccountFallback), address(composabilityHandler)); + _runtime_Value_Injection(address(mockAccount), address(mockAccount)); + _runtime_Value_Injection(address(mockAccountCaller), address(composabilityHandler)); + _runtime_Value_Injection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_Runtime_Target_Injection_Success() public { + _runtime_Target_Injection(address(mockAccountFallback), address(composabilityHandler)); + _runtime_Target_Injection(address(mockAccount), address(mockAccount)); + _runtime_Target_Injection(address(mockAccountCaller), address(composabilityHandler)); + _runtime_Target_Injection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + // ================================================================================= + // ================================ TEST SCENARIOS ================================ + // ================================================================================= + + function _inputStaticCallOutputExecResult(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + // Step 1: Call function A and store its result + // Prepare return value config for function A + InputParam[] memory inputParamsA = new InputParam[](2); + inputParamsA[0] = _createRawTargetInputParam(address(dummyContract)); + inputParamsA[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParamsA = new OutputParam[](1); + outputParamsA[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(1, storageContract, SLOT_A) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.A.selector, + inputParams: inputParamsA, // TARGET and VALUE parameters only + outputParams: outputParamsA // store output of the function A() to the storage + }); + + // Call function A + IComposableExecution(address(account)).executeComposable(executions); + + // Verify the result (42) was stored correctly + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 storedValueA = storageContract.readStorage(namespace, SLOT_A_0); + assertEq(uint256(storedValueA), 42, "Function A result not stored correctly"); + + // Step 2: Call function B using the stored value from A + InputParam[] memory inputParamsB = new InputParam[](3); + inputParamsB[0] = _createRawTargetInputParam(address(dummyContract)); + inputParamsB[1] = _createRawValueInputParam(0); + inputParamsB[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A_0))), + constraints: emptyConstraints + }); + + // Prepare return value config for function B + OutputParam[] memory outputParamsB = new OutputParam[](1); + outputParamsB[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(1, storageContract, SLOT_B) + }); + + ComposableExecution[] memory executionsB = new ComposableExecution[](1); + executionsB[0] = ComposableExecution({ + functionSig: DummyContract.B.selector, + inputParams: inputParamsB, + outputParams: outputParamsB + }); + // Call function B + IComposableExecution(address(account)).executeComposable(executionsB); + + // Verify the result (84 = 42 * 2) was stored correctly + bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); + bytes32 storedValueB = storageContract.readStorage(namespace, SLOT_B_0); + assertEq(uint256(storedValueB), 84, "Function B result not stored correctly"); + + vm.stopPrank(); + } + + // use 1 as input for emitUint256 + // so 1 should be emitted + function _inputRawBytes(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 valueToSendExecution; + if (address(account) == address(mockAccountFallback)) { + valueToSendExecution = 1e15; // make sure value is successfully sent back by compos module + } + + InputParam[] memory inputParams = new InputParam[](3); + // call data + inputParams[0] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(1), + constraints: emptyConstraints + }); + + inputParams[1] = _createRawTargetInputParam(address(dummyContract)); + inputParams[2] = _createRawValueInputParam(valueToSendExecution); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.emitUint256.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(1); + if (address(account) == address(mockAccountFallback)) { + vm.expectEmit(address(dummyContract)); + emit Received(valueToSendExecution); + } + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } + + // test static call output fetcher. + // call getFoo() on dummyContract + // store the result in the composability storage + // and check that the result is stored correctly + function _outputStaticCall(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](1); + outputParams[0] = OutputParam({ + fetcherType: OutputParamFetcherType.STATIC_CALL, + paramData: abi.encode( + 1, + address(dummyContract), + abi.encodeWithSelector(DummyContract.getFoo.selector), + address(storageContract), + SLOT_B + ) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.getFoo.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + uint256 expectedValue = 2517; + dummyContract.setFoo(expectedValue); + assertEq(dummyContract.getFoo(), expectedValue, "Value not stored correctly in the contract itself"); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); + bytes32 storedValue = storageContract.readStorage(namespace, SLOT_B_0); + assertEq(uint256(storedValue), expectedValue, "Value not stored correctly in the composability storage"); + } + + function _useOutputAsInput(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 input1 = 2517; + uint256 input2 = 7579; + uint256 valueToSend = 1e15; + dummyContract.setFoo(input1); + + // first execution => call swap and store the result in the composability storage + InputParam[] memory inputParams_execution1 = new InputParam[](4); + inputParams_execution1[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams_execution1[1] = _createRawValueInputParam(valueToSend); + inputParams_execution1[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(input1), + constraints: emptyConstraints + }); + inputParams_execution1[3] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(input2), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams_execution1 = new OutputParam[](2); + outputParams_execution1[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(1, address(storageContract), SLOT_A) + }); + outputParams_execution1[1] = OutputParam({ + fetcherType: OutputParamFetcherType.STATIC_CALL, + paramData: abi.encode( + 1, + address(dummyContract), + abi.encodeWithSelector(DummyContract.getFoo.selector), + address(storageContract), + SLOT_B + ) + }); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); + // second execution => call stake with the result of the first execution + InputParam[] memory inputParams_execution2 = new InputParam[](4); + inputParams_execution2[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams_execution2[1] = _createRawValueInputParam(valueToSend); + inputParams_execution2[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_A_0))), + constraints: emptyConstraints + }); + inputParams_execution2[3] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(storageContract, abi.encodeCall(Storage.readStorage, (namespace, SLOT_B_0))), + constraints: emptyConstraints + }); + OutputParam[] memory outputParams_execution2 = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](2); + executions[0] = ComposableExecution({ + functionSig: DummyContract.swap.selector, + inputParams: inputParams_execution1, + outputParams: outputParams_execution1 + }); + executions[1] = ComposableExecution({ + functionSig: DummyContract.stake.selector, + inputParams: inputParams_execution2, + outputParams: outputParams_execution2 + }); + + uint256 expectedToStake = input1 + 1; + uint256 messageValue; + + if (address(account) == address(mockAccountFallback)) { + messageValue = valueToSend; + vm.expectEmit(address(mockAccountFallback)); + emit MockAccountReceive(messageValue); + } + + // swap emits input params + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted2(input1, input2); + // swap emits output param + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(expectedToStake); + // stake emits input params: first param is from swap, second param is from getFoo which is just input1 + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted2(expectedToStake, input1); + + vm.expectEmit(address(dummyContract)); + emit Received(valueToSend); + + IComposableExecution(address(account)).executeComposable{value: messageValue}(executions); + + //check storage slots + bytes32 storedValueA = storageContract.readStorage(namespace, SLOT_A_0); + assertEq(uint256(storedValueA), expectedToStake, "Value not stored correctly in the composability storage"); + bytes32 storedValueB = storageContract.readStorage(namespace, SLOT_B_0); + assertEq(uint256(storedValueB), input1, "Value not stored correctly in the composability storage"); + + vm.stopPrank(); + } + + // test that outputExecResultAddress works correctly with address + // call getAddress() on dummyContract + // store the result in the composability storage + // and check that the result is stored correctly + function _outputExecResultAddress(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](1); + outputParams[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(1, address(storageContract), SLOT_A) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.getAddress.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 storedValue = storageContract.readStorage(namespace, SLOT_A_0); + assertEq(address(uint160(uint256(storedValue))), address(dummyContract), "Value not stored correctly in the composability storage"); + } + + function _outputExecResultBool(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + OutputParam[] memory outputParams = new OutputParam[](1); + outputParams[0] = OutputParam({ + fetcherType: OutputParamFetcherType.EXEC_RESULT, + paramData: abi.encode(1, address(storageContract), SLOT_A) + }); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.getBool.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + IComposableExecution(address(account)).executeComposable(executions); + vm.stopPrank(); + + bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); + bytes32 SLOT_A_0 = keccak256(abi.encodePacked(SLOT_A, uint256(0))); + bytes32 storedValue = storageContract.readStorage(namespace, SLOT_A_0); + assertTrue(uint8(uint256(storedValue)) == 1, "Value not stored correctly in the composability storage"); + } + + function _runtime_Value_Injection(address account, address caller) internal { + + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = InputParam({ + paramType: InputParamType.VALUE, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.getNativeValue.selector)), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.payableEmit.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + uint256 expectedValue = dummyContract.getNativeValue(); + vm.expectEmit(address(dummyContract)); + emit Received(expectedValue); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } + + function _runtime_Target_Injection(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 uintToEmit = 1823920923; + + InputParam[] memory inputParams = new InputParam[](3); + inputParams[0] = InputParam({ + paramType: InputParamType.TARGET, + fetcherType: InputParamFetcherType.STATIC_CALL, + paramData: abi.encode(address(dummyContract), abi.encodeWithSelector(DummyContract.getAddress.selector)), + constraints: emptyConstraints + }); + inputParams[1] = _createRawValueInputParam(0); + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.RAW_BYTES, + paramData: abi.encode(uintToEmit), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.emitUint256.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(uintToEmit); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } + + + + + /* + + runtime address injection + + test fetcher type BALANCE + + + */ +} \ No newline at end of file From 4c3c41112a45587898959efc7c2a3e93a5a5de08 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 5 Sep 2025 10:34:22 +0300 Subject: [PATCH 04/11] add final test --- contracts/ComposableExecutionLib.sol | 9 +- test/ComposabilityBase.t.sol | 3 + test/mock/MockAccountCaller.sol | 1 - test/mock/MockERC20Balance.sol | 11 +++ ...ComposableExecution_ConstraintsReverts.sol | 43 ++++++++++ test/unit/ComposableExecution_Simple.t.sol | 82 +++++++++++++++++++ 6 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 test/mock/MockERC20Balance.sol diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index 04f09c6..3843976 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -71,18 +71,19 @@ library ComposableExecutionLib { _validateConstraints(returnData, param.constraints); return returnData; } else if (param.fetcherType == InputParamFetcherType.BALANCE) { - address contractAddr; + address tokenAddr; address account; bytes calldata paramData = param.paramData; + // expect paramData to be abi.encodePacked(address token, address account) assembly { - contractAddr := shr(96, calldataload(paramData.offset)) + tokenAddr := shr(96, calldataload(paramData.offset)) account := shr(96, calldataload(add(paramData.offset, 0x14))) } uint256 balance; - if (contractAddr == address(0)) { + if (tokenAddr == address(0)) { balance = account.balance; } else { - balance = IERC20(contractAddr).balanceOf(account); + balance = IERC20(tokenAddr).balanceOf(account); } _validateConstraints(abi.encode(balance), param.constraints); return abi.encode(balance); diff --git a/test/ComposabilityBase.t.sol b/test/ComposabilityBase.t.sol index e614949..0982569 100644 --- a/test/ComposabilityBase.t.sol +++ b/test/ComposabilityBase.t.sol @@ -11,6 +11,7 @@ import {MockAccount} from "test/mock/MockAccount.sol"; import {Storage} from "../contracts/Storage.sol"; import {InputParam, Constraint, InputParamType, InputParamFetcherType} from "contracts/types/ComposabilityDataTypes.sol"; import "./mock/DummyContract.sol"; +import "./mock/MockERC20Balance.sol"; address constant ENTRYPOINT_V07_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; @@ -21,6 +22,7 @@ contract ComposabilityTestBase is Test { MockAccountCaller internal mockAccountCaller; MockAccountNonRevert internal mockAccountNonRevert; MockAccount internal mockAccount; + MockERC20Balance internal mockERC20Balance; event MockAccountReceive(uint256 amount); Storage public storageContract; @@ -52,6 +54,7 @@ contract ComposabilityTestBase is Test { mockAccount = new MockAccount({_validator: address(0), _handler: address(0xa11ce)}); mockAccountNonRevert = new MockAccountNonRevert({_validator: address(0), _handler: address(0xa11ce)}); + mockERC20Balance = new MockERC20Balance(); // fund accounts vm.deal(address(mockAccountFallback), 100 ether); diff --git a/test/mock/MockAccountCaller.sol b/test/mock/MockAccountCaller.sol index b26c1d1..29f6df6 100644 --- a/test/mock/MockAccountCaller.sol +++ b/test/mock/MockAccountCaller.sol @@ -20,7 +20,6 @@ contract MockAccountCaller is IAccount { event MockAccountFallback(bytes callData, uint256 value); error OnlyExecutor(); - error FallbackFailed(bytes result); IValidator public validator; IFallback public handler; IExecutor public executor; diff --git a/test/mock/MockERC20Balance.sol b/test/mock/MockERC20Balance.sol new file mode 100644 index 0000000..282b2a4 --- /dev/null +++ b/test/mock/MockERC20Balance.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +contract MockERC20Balance { + + mapping(address => uint256) public balanceOf; + + function setBalance(address account, uint256 balance) public { + balanceOf[account] = balance; + } +} \ No newline at end of file diff --git a/test/unit/ComposableExecution_ConstraintsReverts.sol b/test/unit/ComposableExecution_ConstraintsReverts.sol index 0ed2d24..739e47d 100644 --- a/test/unit/ComposableExecution_ConstraintsReverts.sol +++ b/test/unit/ComposableExecution_ConstraintsReverts.sol @@ -10,6 +10,9 @@ import "contracts/types/ComposabilityDataTypes.sol"; contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { + error FallbackFailed(bytes result); + error InvalidParameterEncoding(string message); + function setUp() public override { super.setUp(); } @@ -54,6 +57,13 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { function test_save_Revert_Reason_in_Storage() public { _save_Revert_Reason_in_Storage(address(mockAccountNonRevert), address(mockAccountNonRevert)); } + + function test_Balance_Fetcher_Reverts_If_Used_For_TARGET_Param() public { + _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccountFallback), address(composabilityHandler)); + _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccount), address(mockAccount)); + _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccountCaller), address(composabilityHandler)); + _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } // ================================================================================= // ================================ TEST SCENARIOS ================================ @@ -413,5 +423,38 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { vm.stopPrank(); } + + function _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + InputParam[] memory inputParams = new InputParam[](2); + + inputParams[0] = _createRawValueInputParam(0); + + inputParams[1] = InputParam({ + paramType: InputParamType.TARGET, + fetcherType: InputParamFetcherType.BALANCE, + paramData: abi.encodePacked(address(0), address(0xa11ce)), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: "", + inputParams: inputParams, + outputParams: outputParams + }); + + if (address(account) == address(mockAccountFallback)) { + vm.expectRevert(abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(InvalidParameterEncoding.selector, "BALANCE fetcher type is not supported for TARGET param type"))); + } else { + vm.expectRevert(abi.encodeWithSelector(InvalidParameterEncoding.selector, "BALANCE fetcher type is not supported for TARGET param type")); + } + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } } diff --git a/test/unit/ComposableExecution_Simple.t.sol b/test/unit/ComposableExecution_Simple.t.sol index 5f80224..80f8330 100644 --- a/test/unit/ComposableExecution_Simple.t.sol +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -78,6 +78,20 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { _runtime_Target_Injection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); } + function test_ERC20_Balance_Fetcher_Success() public { + _erc20_Balance_Fetcher(address(mockAccountFallback), address(composabilityHandler)); + _erc20_Balance_Fetcher(address(mockAccount), address(mockAccount)); + _erc20_Balance_Fetcher(address(mockAccountCaller), address(composabilityHandler)); + _erc20_Balance_Fetcher(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + + function test_Native_Balance_Fetcher_Success() public { + _native_Balance_Fetcher(address(mockAccountFallback), address(composabilityHandler)); + _native_Balance_Fetcher(address(mockAccount), address(mockAccount)); + _native_Balance_Fetcher(address(mockAccountCaller), address(composabilityHandler)); + _native_Balance_Fetcher(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); + } + // ================================================================================= // ================================ TEST SCENARIOS ================================ // ================================================================================= @@ -469,6 +483,74 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { vm.stopPrank(); } + + function _erc20_Balance_Fetcher(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 balanceToSet = 139122330912355; + mockERC20Balance.setBalance(address(0xa11ce), balanceToSet); + + InputParam[] memory inputParams = new InputParam[](3); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.BALANCE, + paramData: abi.encodePacked(address(mockERC20Balance), address(0xa11ce)), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.emitUint256.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + // balance is used as param to the emitUint256 function + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(balanceToSet); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } + + function _native_Balance_Fetcher(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 balanceToSet = 139122330912355; + vm.deal(address(0xa11ce), balanceToSet); + assertEq(address(0xa11ce).balance, balanceToSet); + + InputParam[] memory inputParams = new InputParam[](3); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = _createRawValueInputParam(0); + + inputParams[2] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.BALANCE, + paramData: abi.encodePacked(address(0), address(0xa11ce)), + constraints: emptyConstraints + }); + + OutputParam[] memory outputParams = new OutputParam[](0); + + ComposableExecution[] memory executions = new ComposableExecution[](1); + executions[0] = ComposableExecution({ + functionSig: DummyContract.emitUint256.selector, + inputParams: inputParams, + outputParams: outputParams + }); + + vm.expectEmit(address(dummyContract)); + emit Uint256Emitted(balanceToSet); + IComposableExecution(address(account)).executeComposable(executions); + + vm.stopPrank(); + } From 9cdce3b6657f9070c7b8e544f809ec5a95332d98 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 5 Sep 2025 11:30:39 +0300 Subject: [PATCH 05/11] add imports --- contracts/ComposableExecutionBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index 9b5996c..de99604 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; -import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; +import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, InputParamTypeOutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; import {IComposableExecution} from "./interfaces/IComposableExecution.sol"; import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; From 1c309a81886126e4b4adea0638d6bdc120845161 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 5 Sep 2025 11:32:01 +0300 Subject: [PATCH 06/11] add imports --- contracts/ComposableExecutionBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index de99604..dfead40 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; -import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, InputParamTypeOutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; +import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, InputParamType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; import {IComposableExecution} from "./interfaces/IComposableExecution.sol"; import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; From 4f9615a62372e4c49ddf587ddb3af5a0f44c8841 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 9 Sep 2025 13:19:34 +0300 Subject: [PATCH 07/11] chore: small fixes and comments --- contracts/ComposableExecutionLib.sol | 3 ++- test/unit/ComposableExecution_Simple.t.sol | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index 3843976..a9419fb 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -43,7 +43,7 @@ library ComposableExecutionLib { } return Execution({ target: composedTarget, - value: composedValue, + value: composedValue, // if a param with VALUE type was not provided, it will be 0 callData: composedCalldata }); } @@ -57,6 +57,7 @@ library ComposableExecutionLib { address contractAddr; bytes calldata callData; bytes calldata paramData = param.paramData; + // expect paramData to be abi.encode(address contractAddr, bytes callData) assembly { contractAddr := calldataload(paramData.offset) let s := calldataload(add(paramData.offset, 0x20)) diff --git a/test/unit/ComposableExecution_Simple.t.sol b/test/unit/ComposableExecution_Simple.t.sol index 80f8330..e0f8820 100644 --- a/test/unit/ComposableExecution_Simple.t.sol +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -490,17 +490,17 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { uint256 balanceToSet = 139122330912355; mockERC20Balance.setBalance(address(0xa11ce), balanceToSet); - InputParam[] memory inputParams = new InputParam[](3); + InputParam[] memory inputParams = new InputParam[](2); inputParams[0] = _createRawTargetInputParam(address(dummyContract)); - inputParams[1] = _createRawValueInputParam(0); - - inputParams[2] = InputParam({ + inputParams[1] = InputParam({ paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.BALANCE, paramData: abi.encodePacked(address(mockERC20Balance), address(0xa11ce)), constraints: emptyConstraints }); + //inputParams[2] = _createRawValueInputParam(0); + OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); From 550a3d9395baa210395040aa7e3ca679a898d8e6 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 9 Oct 2025 13:01:36 +0300 Subject: [PATCH 08/11] chore: inline comment --- test/unit/ComposableExecution_Simple.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/ComposableExecution_Simple.t.sol b/test/unit/ComposableExecution_Simple.t.sol index e0f8820..a737c60 100644 --- a/test/unit/ComposableExecution_Simple.t.sol +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -499,7 +499,9 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { constraints: emptyConstraints }); - //inputParams[2] = _createRawValueInputParam(0); + // since this is commented out, this test case also + // makes sure that the value = 0 is used if the VALUE param is not provided + // inputParams[2] = _createRawValueInputParam(0); OutputParam[] memory outputParams = new OutputParam[](0); From 08fe4da9f20edecfcdbfd6881bff9231597de2e0 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 9 Oct 2025 15:36:22 +0300 Subject: [PATCH 09/11] chore: add checks and format --- contracts/ComposableExecutionBase.sol | 22 ++- contracts/ComposableExecutionLib.sol | 65 +++++--- contracts/ComposableExecutionModule.sol | 49 +++--- contracts/interfaces/IComposableExecution.sol | 2 +- contracts/types/ComposabilityDataTypes.sol | 4 + test/ComposabilityBase.t.sol | 42 ++--- test/mock/DummyContract.sol | 11 +- test/mock/MockAccount.sol | 56 +++---- test/mock/MockAccountCaller.sol | 58 +++---- test/mock/MockAccountDelegateCaller.sol | 9 +- test/mock/MockAccountFallback.sol | 61 +++----- test/mock/MockAccountNonRevert.sol | 56 +++---- test/mock/MockERC20Balance.sol | 5 +- test/unit/ComposableExecution_Complex.sol | 76 +++------ ...ComposableExecution_ConstraintsReverts.sol | 120 +++++++------- test/unit/ComposableExecution_Simple.t.sol | 147 +++++------------- 16 files changed, 302 insertions(+), 481 deletions(-) diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index dfead40..48c0ddd 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -1,10 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; -import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, InputParamType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; -import {IComposableExecution} from "./interfaces/IComposableExecution.sol"; -import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; +import { ComposableExecutionLib } from "./ComposableExecutionLib.sol"; +import { + InputParam, + OutputParam, + ComposableExecution, + Constraint, + ConstraintType, + InputParamFetcherType, + InputParamType, + OutputParamFetcherType +} from "./types/ComposabilityDataTypes.sol"; +import { IComposableExecution } from "./interfaces/IComposableExecution.sol"; +import { Execution } from "erc7579/interfaces/IERC7579Account.sol"; abstract contract ComposableExecutionBase is IComposableExecution { using ComposableExecutionLib for InputParam[]; @@ -38,8 +47,5 @@ abstract contract ComposableExecutionBase is IComposableExecution { /// using account's native execution approach /// we do not use Execution struct as an argument to be as less opinionated as possible /// instead we just use standard types - function _executeAction(address to, uint256 value, bytes memory data) - internal - virtual - returns (bytes memory returnData); + function _executeAction(address to, uint256 value, bytes memory data) internal virtual returns (bytes memory returnData); } diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index a9419fb..23f3eae 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -1,39 +1,56 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {Storage} from "./Storage.sol"; -import {InputParam, OutputParam, Constraint, ConstraintType, InputParamType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; -import {Execution} from "erc7579/interfaces/IERC7579Account.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Storage } from "./Storage.sol"; +import { + InputParam, + OutputParam, + Constraint, + ConstraintType, + InputParamType, + InputParamFetcherType, + OutputParamFetcherType +} from "./types/ComposabilityDataTypes.sol"; +import { Execution } from "erc7579/interfaces/IERC7579Account.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Library for composable execution handling library ComposableExecutionLib { - error ConstraintNotMet(ConstraintType constraintType); error Output_StaticCallFailed(); error InvalidParameterEncoding(string message); error InvalidOutputParamFetcherType(); error ComposableExecutionFailed(); error InvalidConstraintType(); + error InvalidSetOfInputParams(string message); // Process the input parameters and return the composed calldata - function processInputs(InputParam[] calldata inputParams, bytes4 functionSig) - internal - view - returns (Execution memory) - { + function processInputs(InputParam[] calldata inputParams, bytes4 functionSig) internal view returns (Execution memory) { address composedTarget; uint256 composedValue; bytes memory composedCalldata = abi.encodePacked(functionSig); uint256 length = inputParams.length; + + // Bit 0: TARGET param type set, Bit 1: VALUE param type set + uint256 paramTypeFlags = 0; for (uint256 i; i < length; i++) { bytes memory processedInput = processInput(inputParams[i]); if (inputParams[i].paramType == InputParamType.TARGET) { if (inputParams[i].fetcherType == InputParamFetcherType.BALANCE) { - revert InvalidParameterEncoding("BALANCE fetcher type is not supported for TARGET param type"); + revert InvalidParameterEncoding("BALANCE fetcher type is not supported for TARGET param type"); + } + // Check if TARGET has already been set (bit 0) + if (paramTypeFlags & 1 != 0) { + revert InvalidSetOfInputParams("TARGET param type can only be set once"); } + paramTypeFlags |= 1; // Set bit 0 composedTarget = abi.decode(processedInput, (address)); } else if (inputParams[i].paramType == InputParamType.VALUE) { + // Check if VALUE has already been set (bit 1) + if (paramTypeFlags & 2 != 0) { + revert InvalidSetOfInputParams("VALUE param type can only be set once"); + } + paramTypeFlags |= 2; // Set bit 1 composedValue = abi.decode(processedInput, (uint256)); } else if (inputParams[i].paramType == InputParamType.CALL_DATA) { composedCalldata = bytes.concat(composedCalldata, processedInput); @@ -41,6 +58,9 @@ library ComposableExecutionLib { revert InvalidParameterEncoding("Invalid param type"); } } + if (composedTarget == address(0)) { + revert InvalidSetOfInputParams("TARGET InputParam is required"); + } return Execution({ target: composedTarget, value: composedValue, // if a param with VALUE type was not provided, it will be 0 @@ -116,7 +136,7 @@ library ComposableExecutionLib { targetStorageSlot := calldataload(add(paramData.offset, 0x40)) } _parseReturnDataAndWriteToStorage(returnValues, returnData, targetStorageContract, targetStorageSlot, account); - // same for static calls + // same for static calls } else if (param.fetcherType == OutputParamFetcherType.STATIC_CALL) { uint256 returnValues; address sourceContract; @@ -145,10 +165,7 @@ library ComposableExecutionLib { } /// @dev Validate the constraints => compare the value with the reference data - function _validateConstraints(bytes memory rawValue, Constraint[] calldata constraints) - private - pure - { + function _validateConstraints(bytes memory rawValue, Constraint[] calldata constraints) private pure { if (constraints.length > 0) { for (uint256 i; i < constraints.length; i++) { Constraint memory constraint = constraints[i]; @@ -173,17 +190,21 @@ library ComposableExecutionLib { } /// @dev Parse the return data and write to the appropriate storage contract - function _parseReturnDataAndWriteToStorage(uint256 returnValues, bytes memory returnData, address targetStorageContract, bytes32 targetStorageSlot, address account) internal { + function _parseReturnDataAndWriteToStorage( + uint256 returnValues, + bytes memory returnData, + address targetStorageContract, + bytes32 targetStorageSlot, + address account + ) + internal + { for (uint256 i; i < returnValues; i++) { bytes32 value; assembly { value := mload(add(returnData, add(0x20, mul(i, 0x20)))) } - Storage(targetStorageContract).writeStorage({ - slot: keccak256(abi.encodePacked(targetStorageSlot, i)), - value: value, - account: account - }); + Storage(targetStorageContract).writeStorage({ slot: keccak256(abi.encodePacked(targetStorageSlot, i)), value: value, account: account }); } } } diff --git a/contracts/ComposableExecutionModule.sol b/contracts/ComposableExecutionModule.sol index 315f22c..63c0e9a 100644 --- a/contracts/ComposableExecutionModule.sol +++ b/contracts/ComposableExecutionModule.sol @@ -1,21 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {IExecutor} from "erc7579/interfaces/IERC7579Module.sol"; -import {IERC7579Account, Execution} from "erc7579/interfaces/IERC7579Account.sol"; -import {ModeLib} from "erc7579/lib/ModeLib.sol"; -import {ExecutionLib} from "erc7579/lib/ExecutionLib.sol"; -import {ERC7579FallbackBase} from "module-bases/ERC7579FallbackBase.sol"; -import {IComposableExecutionModule} from "./interfaces/IComposableExecution.sol"; -import {ComposableExecutionLib} from "./ComposableExecutionLib.sol"; -import {InputParam, OutputParam, ComposableExecution, Constraint, ConstraintType, InputParamFetcherType, OutputParamFetcherType} from "./types/ComposabilityDataTypes.sol"; +import { IExecutor } from "erc7579/interfaces/IERC7579Module.sol"; +import { IERC7579Account, Execution } from "erc7579/interfaces/IERC7579Account.sol"; +import { ModeLib } from "erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "erc7579/lib/ExecutionLib.sol"; +import { ERC7579FallbackBase } from "module-bases/ERC7579FallbackBase.sol"; +import { IComposableExecutionModule } from "./interfaces/IComposableExecution.sol"; +import { ComposableExecutionLib } from "./ComposableExecutionLib.sol"; +import { + InputParam, + OutputParam, + ComposableExecution, + Constraint, + ConstraintType, + InputParamFetcherType, + OutputParamFetcherType +} from "./types/ComposabilityDataTypes.sol"; /** * @title Composable Execution Module: Executor and Fallback * @dev A module for ERC-7579 accounts that enables composable transactions execution */ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC7579FallbackBase { - address private constant ENTRY_POINT_V07_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; address public immutable DEFAULT_EP_ADDRESS; address private immutable THIS_ADDRESS; @@ -52,9 +59,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC address sender = _msgSender(); // in most cases, only first condition (against constant) will be checked // so no extra sloads - require(sender == DEFAULT_EP_ADDRESS || - sender == entryPoints[msg.sender] || - sender == msg.sender, OnlyEntryPointOrAccount()); + require(sender == DEFAULT_EP_ADDRESS || sender == entryPoints[msg.sender] || sender == msg.sender, OnlyEntryPointOrAccount()); _returnMsgValue(); _executeComposable(cExecutions, msg.sender, _executeExecutionCall); } @@ -62,7 +67,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC /// @notice It doesn't require access control as it is expected to be called by the account itself via .execute() /// @dev !!! Attention !!! This function should NEVER be installed to be used via fallback() as it doesn't implement access control /// thus it will be callable by any address account.executeComposableCall => fallback() => this.executeComposableCall - function executeComposableCall(ComposableExecution[] calldata cExecutions) external { + function executeComposableCall(ComposableExecution[] calldata cExecutions) external { _executeComposable(cExecutions, msg.sender, _executeExecutionCall); } @@ -80,14 +85,16 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC ComposableExecution[] calldata cExecutions, address account, function(Execution memory execution) internal returns(bytes[] memory) executeExecutionFunction - ) internal { + ) + internal + { // we can not use erc-7579 batch mode here because we may need to compose // the next call in the batch based on the execution result of the previous call uint256 length = cExecutions.length; for (uint256 i; i < length; i++) { ComposableExecution calldata cExecution = cExecutions[i]; Execution memory execution = cExecution.inputParams.processInputs(cExecution.functionSig); - bytes[] memory returnData; + bytes[] memory returnData; if (execution.target != address(0)) { returnData = executeExecutionFunction(execution); } else { @@ -101,9 +108,9 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC /// @dev function to be used as an argument for _executeComposable in case of regular call function _executeExecutionCall(Execution memory execution) internal returns (bytes[] memory) { return IERC7579Account(msg.sender).executeFromExecutor({ - mode: ModeLib.encodeSimpleSingle(), - executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData) - }); + mode: ModeLib.encodeSimpleSingle(), + executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData) + }); } /// @dev function to be used as an argument for _executeComposable in case of delegatecall @@ -117,7 +124,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC require(_entryPoint != address(0), ZeroAddressNotAllowed()); entryPoints[msg.sender] = _entryPoint; } - + /// @dev returns the entry point address function getEntryPoint(address account) external view returns (address) { return entryPoints[account] == address(0) ? DEFAULT_EP_ADDRESS : entryPoints[account]; @@ -170,13 +177,13 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC let o := add(result, 0x20) returndatacopy(o, 0x00, returndatasize()) // Copy the returndata. mstore(0x40, add(o, returndatasize())) // Allocate the memory. - } + } } // Returns the msg.value back to the sender (account) function _returnMsgValue() internal { if (msg.value > 0) { - (bool success, ) = payable(msg.sender).call{value: msg.value}(""); + (bool success,) = payable(msg.sender).call{ value: msg.value }(""); require(success, FailedToReturnMsgValue()); } } diff --git a/contracts/interfaces/IComposableExecution.sol b/contracts/interfaces/IComposableExecution.sol index 56871d9..51da00f 100644 --- a/contracts/interfaces/IComposableExecution.sol +++ b/contracts/interfaces/IComposableExecution.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.23; -import {ComposableExecution} from "../types/ComposabilityDataTypes.sol"; +import { ComposableExecution } from "../types/ComposabilityDataTypes.sol"; interface IComposableExecution { function executeComposable(ComposableExecution[] calldata cExecutions) external payable; diff --git a/contracts/types/ComposabilityDataTypes.sol b/contracts/types/ComposabilityDataTypes.sol index 2004d22..e3e964f 100644 --- a/contracts/types/ComposabilityDataTypes.sol +++ b/contracts/types/ComposabilityDataTypes.sol @@ -6,6 +6,7 @@ enum InputParamType { TARGET, // The target address VALUE, // The value CALL_DATA // The call data + } // Parameter type for composition @@ -13,11 +14,13 @@ enum InputParamFetcherType { RAW_BYTES, // Already encoded bytes STATIC_CALL, // Perform a static call BALANCE // Get the balance of an address + } enum OutputParamFetcherType { EXEC_RESULT, // The return of the execution call STATIC_CALL // Call to some other function + } // Constraint type for parameter validation @@ -26,6 +29,7 @@ enum ConstraintType { GTE, // Greater than or equal to LTE, // Less than or equal to IN // In range + } // Constraint for parameter validation diff --git a/test/ComposabilityBase.t.sol b/test/ComposabilityBase.t.sol index 0982569..ad2deac 100644 --- a/test/ComposabilityBase.t.sol +++ b/test/ComposabilityBase.t.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {Test, Vm, console2} from "forge-std/Test.sol"; -import {MockAccountFallback} from "./mock/MockAccountFallback.sol"; -import {MockAccountNonRevert} from "./mock/MockAccountNonRevert.sol"; -import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; -import {MockAccountDelegateCaller} from "./mock/MockAccountDelegateCaller.sol"; -import {MockAccountCaller} from "./mock/MockAccountCaller.sol"; -import {MockAccount} from "test/mock/MockAccount.sol"; -import {Storage} from "../contracts/Storage.sol"; -import {InputParam, Constraint, InputParamType, InputParamFetcherType} from "contracts/types/ComposabilityDataTypes.sol"; +import { Test, Vm, console2 } from "forge-std/Test.sol"; +import { MockAccountFallback } from "./mock/MockAccountFallback.sol"; +import { MockAccountNonRevert } from "./mock/MockAccountNonRevert.sol"; +import { ComposableExecutionModule } from "contracts/ComposableExecutionModule.sol"; +import { MockAccountDelegateCaller } from "./mock/MockAccountDelegateCaller.sol"; +import { MockAccountCaller } from "./mock/MockAccountCaller.sol"; +import { MockAccount } from "test/mock/MockAccount.sol"; +import { Storage } from "../contracts/Storage.sol"; +import { InputParam, Constraint, InputParamType, InputParamFetcherType } from "contracts/types/ComposabilityDataTypes.sol"; import "./mock/DummyContract.sol"; import "./mock/MockERC20Balance.sol"; @@ -25,6 +25,7 @@ contract ComposabilityTestBase is Test { MockERC20Balance internal mockERC20Balance; event MockAccountReceive(uint256 amount); + Storage public storageContract; DummyContract public dummyContract; @@ -35,25 +36,16 @@ contract ComposabilityTestBase is Test { function setUp() public virtual { composabilityHandler = new ComposableExecutionModule(ENTRYPOINT_V07_ADDRESS); - mockAccountFallback = new MockAccountFallback({ - _validator: address(0), - _executor: address(composabilityHandler), - _handler: address(composabilityHandler) - }); - mockAccountCaller = new MockAccountCaller({ - _validator: address(0), - _executor: address(composabilityHandler), - _handler: address(composabilityHandler) - }); - mockAccountDelegateCaller = new MockAccountDelegateCaller({ - _composableModule: address(composabilityHandler) - }); + mockAccountFallback = + new MockAccountFallback({ _validator: address(0), _executor: address(composabilityHandler), _handler: address(composabilityHandler) }); + mockAccountCaller = new MockAccountCaller({ _validator: address(0), _executor: address(composabilityHandler), _handler: address(composabilityHandler) }); + mockAccountDelegateCaller = new MockAccountDelegateCaller({ _composableModule: address(composabilityHandler) }); vm.prank(address(mockAccountFallback)); composabilityHandler.onInstall(abi.encodePacked(ENTRYPOINT_V07_ADDRESS)); - mockAccount = new MockAccount({_validator: address(0), _handler: address(0xa11ce)}); - mockAccountNonRevert = new MockAccountNonRevert({_validator: address(0), _handler: address(0xa11ce)}); + mockAccount = new MockAccount({ _validator: address(0), _handler: address(0xa11ce) }); + mockAccountNonRevert = new MockAccountNonRevert({ _validator: address(0), _handler: address(0xa11ce) }); mockERC20Balance = new MockERC20Balance(); // fund accounts @@ -86,6 +78,4 @@ contract ComposabilityTestBase is Test { constraints: emptyConstraints }); } - - } diff --git a/test/mock/DummyContract.sol b/test/mock/DummyContract.sol index 9009a82..9999d28 100644 --- a/test/mock/DummyContract.sol +++ b/test/mock/DummyContract.sol @@ -3,11 +3,17 @@ pragma solidity ^0.8.23; event Uint256Emitted(uint256 value); + event Uint256Emitted2(uint256 value1, uint256 value2); + event AddressEmitted(address addr); + event Bytes32Emitted(bytes32 slot); + event BoolEmitted(bool flag); + event BytesEmitted(bytes data); + event Received(uint256 amount); error DummyRevert(uint256 value); @@ -22,7 +28,6 @@ struct MockSwapStruct { } contract DummyContract { - uint256 internal foo; function A() external pure returns (uint256) { @@ -35,7 +40,7 @@ contract DummyContract { } function getNativeValue() external pure returns (uint256) { - return 10491; // 10491 wei + return 10_491; // 10491 wei } function getFoo() external view returns (uint256) { @@ -105,4 +110,4 @@ contract DummyContract { function payableEmit() external payable { emit Received(msg.value); } -} \ No newline at end of file +} diff --git a/test/mock/MockAccount.sol b/test/mock/MockAccount.sol index b398c44..bf2575b 100644 --- a/test/mock/MockAccount.sol +++ b/test/mock/MockAccount.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {IAccount} from "account-abstraction/interfaces/IAccount.sol"; -import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol"; -import {IValidator, IFallback} from "erc7579/interfaces/IERC7579Module.sol"; -import {IStatelessValidator} from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; -import {EIP1271_SUCCESS, EIP1271_FAILED} from "contracts/types/Constants.sol"; -import {ERC2771Lib} from "./lib/ERC2771Lib.sol"; -import {ComposableExecutionBase} from "contracts/ComposableExecutionBase.sol"; -import {ComposableExecution} from "contracts/types/ComposabilityDataTypes.sol"; +import { IAccount } from "account-abstraction/interfaces/IAccount.sol"; +import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol"; +import { IValidator, IFallback } from "erc7579/interfaces/IERC7579Module.sol"; +import { IStatelessValidator } from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; +import { EIP1271_SUCCESS, EIP1271_FAILED } from "contracts/types/Constants.sol"; +import { ERC2771Lib } from "./lib/ERC2771Lib.sol"; +import { ComposableExecutionBase } from "contracts/ComposableExecutionBase.sol"; +import { ComposableExecution } from "contracts/types/ComposabilityDataTypes.sol"; -import {console2} from "forge-std/console2.sol"; +import { console2 } from "forge-std/console2.sol"; address constant ENTRY_POINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; @@ -31,10 +31,7 @@ contract MockAccount is ComposableExecutionBase, IAccount { handler = IFallback(_handler); } - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 vd) - { + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 vd) { if (address(validator) != address(0)) { vd = validator.validateUserOp(userOp, userOpHash); } @@ -42,28 +39,16 @@ contract MockAccount is ComposableExecutionBase, IAccount { } function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - return - IValidator(address(validator)).isValidSignatureWithSender({sender: msg.sender, hash: hash, data: signature}); + return IValidator(address(validator)).isValidSignatureWithSender({ sender: msg.sender, hash: hash, data: signature }); } - function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) - external - view - returns (bool) - { - return IStatelessValidator(address(validator)).validateSignatureWithData({ - hash: signedHash, - signature: signature, - data: signerData - }); + function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) external view returns (bool) { + return IStatelessValidator(address(validator)).validateSignatureWithData({ hash: signedHash, signature: signature, data: signerData }); } - function execute(address to, uint256 value, bytes calldata data) - external - returns (bool success, bytes memory result) - { + function execute(address to, uint256 value, bytes calldata data) external returns (bool success, bytes memory result) { emit MockAccountExecute(to, value, data); - (success, result) = to.call{value: value}(data); + (success, result) = to.call{ value: value }(data); } function executeComposable(ComposableExecution[] calldata cExecutions) external payable override { @@ -71,13 +56,9 @@ contract MockAccount is ComposableExecutionBase, IAccount { _executeComposable(cExecutions); } - function _executeAction(address to, uint256 value, bytes memory data) - internal - override - returns (bytes memory returnData) - { + function _executeAction(address to, uint256 value, bytes memory data) internal override returns (bytes memory returnData) { bool success; - (success, returnData) = to.call{value: value}(data); + (success, returnData) = to.call{ value: value }(data); if (!success) { revert ExecutionFailed(); } @@ -88,8 +69,7 @@ contract MockAccount is ComposableExecutionBase, IAccount { } fallback(bytes calldata callData) external payable returns (bytes memory) { - (bool success, bytes memory result) = - address(handler).call{value: msg.value}(ERC2771Lib.get2771CallData(callData)); + (bool success, bytes memory result) = address(handler).call{ value: msg.value }(ERC2771Lib.get2771CallData(callData)); if (!success) { revert(string(result)); } diff --git a/test/mock/MockAccountCaller.sol b/test/mock/MockAccountCaller.sol index 29f6df6..5f32fac 100644 --- a/test/mock/MockAccountCaller.sol +++ b/test/mock/MockAccountCaller.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {IAccount} from "account-abstraction/interfaces/IAccount.sol"; -import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol"; -import {IValidator, IFallback, IExecutor} from "erc7579/interfaces/IERC7579Module.sol"; -import {IStatelessValidator} from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; -import {EIP1271_SUCCESS, EIP1271_FAILED} from "contracts/types/Constants.sol"; -import {ERC2771Lib} from "./lib/ERC2771Lib.sol"; -import {ExecutionLib} from "erc7579/lib/ExecutionLib.sol"; -import {ModeLib, ModeCode as ExecutionMode, CallType, ExecType, CALLTYPE_SINGLE} from "erc7579/lib/ModeLib.sol"; +import { IAccount } from "account-abstraction/interfaces/IAccount.sol"; +import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol"; +import { IValidator, IFallback, IExecutor } from "erc7579/interfaces/IERC7579Module.sol"; +import { IStatelessValidator } from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; +import { EIP1271_SUCCESS, EIP1271_FAILED } from "contracts/types/Constants.sol"; +import { ERC2771Lib } from "./lib/ERC2771Lib.sol"; +import { ExecutionLib } from "erc7579/lib/ExecutionLib.sol"; +import { ModeLib, ModeCode as ExecutionMode, CallType, ExecType, CALLTYPE_SINGLE } from "erc7579/lib/ModeLib.sol"; import "contracts/interfaces/IComposableExecution.sol"; -import {console2} from "forge-std/console2.sol"; +import { console2 } from "forge-std/console2.sol"; contract MockAccountCaller is IAccount { event MockAccountValidateUserOp(PackedUserOperation userOp, bytes32 userOpHash, uint256 missingAccountFunds); @@ -20,6 +20,7 @@ contract MockAccountCaller is IAccount { event MockAccountFallback(bytes callData, uint256 value); error OnlyExecutor(); + IValidator public validator; IFallback public handler; IExecutor public executor; @@ -40,10 +41,7 @@ contract MockAccountCaller is IAccount { IComposableExecutionModule(address(handler)).executeComposableCall(cExecutions); } - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 vd) - { + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 vd) { if (address(validator) != address(0)) { vd = validator.validateUserOp(userOp, userOpHash); } @@ -51,35 +49,19 @@ contract MockAccountCaller is IAccount { } function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - return - IValidator(address(validator)).isValidSignatureWithSender({sender: msg.sender, hash: hash, data: signature}); + return IValidator(address(validator)).isValidSignatureWithSender({ sender: msg.sender, hash: hash, data: signature }); } - function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) - external - view - returns (bool) - { - return IStatelessValidator(address(validator)).validateSignatureWithData({ - hash: signedHash, - signature: signature, - data: signerData - }); + function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) external view returns (bool) { + return IStatelessValidator(address(validator)).validateSignatureWithData({ hash: signedHash, signature: signature, data: signerData }); } - function execute(address to, uint256 value, bytes calldata data) - external - returns (bool success, bytes memory result) - { + function execute(address to, uint256 value, bytes calldata data) external returns (bool success, bytes memory result) { emit MockAccountExecute(to, value, data); - (success, result) = to.call{value: value}(data); + (success, result) = to.call{ value: value }(data); } - function executeFromExecutor(ExecutionMode mode, bytes calldata executionCalldata) - external - payable - returns (bytes[] memory returnData) - { + function executeFromExecutor(ExecutionMode mode, bytes calldata executionCalldata) external payable returns (bytes[] memory returnData) { require(msg.sender == address(executor), OnlyExecutor()); (CallType callType, ExecType execType,,) = mode.decode(); @@ -93,11 +75,7 @@ contract MockAccountCaller is IAccount { } } - function _execute(address target, uint256 value, bytes calldata callData) - internal - virtual - returns (bytes memory result) - { + function _execute(address target, uint256 value, bytes calldata callData) internal virtual returns (bytes memory result) { /// @solidity memory-safe-assembly assembly { result := mload(0x40) diff --git a/test/mock/MockAccountDelegateCaller.sol b/test/mock/MockAccountDelegateCaller.sol index 31d58ba..c84e920 100644 --- a/test/mock/MockAccountDelegateCaller.sol +++ b/test/mock/MockAccountDelegateCaller.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.23; import "contracts/interfaces/IComposableExecution.sol"; contract MockAccountDelegateCaller { - address composableModule; + event MockAccountDelegateCall(bytes returnData); constructor(address _composableModule) { @@ -14,12 +14,11 @@ contract MockAccountDelegateCaller { function executeComposable(ComposableExecution[] calldata cExecutions) external payable { // delegatecall to the composableModule - (bool success, bytes memory returnData) = composableModule.delegatecall(abi.encodeWithSelector(IComposableExecutionModule.executeComposableDelegateCall.selector, cExecutions)); + (bool success, bytes memory returnData) = + composableModule.delegatecall(abi.encodeWithSelector(IComposableExecutionModule.executeComposableDelegateCall.selector, cExecutions)); emit MockAccountDelegateCall(returnData); assembly { - if iszero(success) { - revert(add(returnData, 0x20), mload(returnData)) - } + if iszero(success) { revert(add(returnData, 0x20), mload(returnData)) } } } } diff --git a/test/mock/MockAccountFallback.sol b/test/mock/MockAccountFallback.sol index 1475802..0899095 100644 --- a/test/mock/MockAccountFallback.sol +++ b/test/mock/MockAccountFallback.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {IAccount} from "account-abstraction/interfaces/IAccount.sol"; -import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol"; -import {IValidator, IFallback, IExecutor} from "erc7579/interfaces/IERC7579Module.sol"; -import {IStatelessValidator} from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; -import {EIP1271_SUCCESS, EIP1271_FAILED} from "contracts/types/Constants.sol"; -import {ERC2771Lib} from "./lib/ERC2771Lib.sol"; -import {ExecutionLib} from "erc7579/lib/ExecutionLib.sol"; -import {ModeLib, ModeCode as ExecutionMode, CallType, ExecType, CALLTYPE_SINGLE} from "erc7579/lib/ModeLib.sol"; +import { IAccount } from "account-abstraction/interfaces/IAccount.sol"; +import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol"; +import { IValidator, IFallback, IExecutor } from "erc7579/interfaces/IERC7579Module.sol"; +import { IStatelessValidator } from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; +import { EIP1271_SUCCESS, EIP1271_FAILED } from "contracts/types/Constants.sol"; +import { ERC2771Lib } from "./lib/ERC2771Lib.sol"; +import { ExecutionLib } from "erc7579/lib/ExecutionLib.sol"; +import { ModeLib, ModeCode as ExecutionMode, CallType, ExecType, CALLTYPE_SINGLE } from "erc7579/lib/ModeLib.sol"; import "contracts/interfaces/IComposableExecution.sol"; -import {console2} from "forge-std/console2.sol"; +import { console2 } from "forge-std/console2.sol"; contract MockAccountFallback is IAccount { event MockAccountValidateUserOp(PackedUserOperation userOp, bytes32 userOpHash, uint256 missingAccountFunds); @@ -21,6 +21,7 @@ contract MockAccountFallback is IAccount { error OnlyExecutor(); error FallbackFailed(bytes result); + IValidator public validator; IFallback public handler; IExecutor public executor; @@ -34,10 +35,7 @@ contract MockAccountFallback is IAccount { handler = IFallback(_handler); } - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 vd) - { + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 vd) { if (address(validator) != address(0)) { vd = validator.validateUserOp(userOp, userOpHash); } @@ -45,35 +43,19 @@ contract MockAccountFallback is IAccount { } function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - return - IValidator(address(validator)).isValidSignatureWithSender({sender: msg.sender, hash: hash, data: signature}); + return IValidator(address(validator)).isValidSignatureWithSender({ sender: msg.sender, hash: hash, data: signature }); } - function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) - external - view - returns (bool) - { - return IStatelessValidator(address(validator)).validateSignatureWithData({ - hash: signedHash, - signature: signature, - data: signerData - }); + function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) external view returns (bool) { + return IStatelessValidator(address(validator)).validateSignatureWithData({ hash: signedHash, signature: signature, data: signerData }); } - function execute(address to, uint256 value, bytes calldata data) - external - returns (bool success, bytes memory result) - { + function execute(address to, uint256 value, bytes calldata data) external returns (bool success, bytes memory result) { emit MockAccountExecute(to, value, data); - (success, result) = to.call{value: value}(data); + (success, result) = to.call{ value: value }(data); } - function executeFromExecutor(ExecutionMode mode, bytes calldata executionCalldata) - external - payable - returns (bytes[] memory returnData) - { + function executeFromExecutor(ExecutionMode mode, bytes calldata executionCalldata) external payable returns (bytes[] memory returnData) { require(msg.sender == address(executor), OnlyExecutor()); (CallType callType, ExecType execType,,) = mode.decode(); @@ -87,11 +69,7 @@ contract MockAccountFallback is IAccount { } } - function _execute(address target, uint256 value, bytes calldata callData) - internal - virtual - returns (bytes memory result) - { + function _execute(address target, uint256 value, bytes calldata callData) internal virtual returns (bytes memory result) { /// @solidity memory-safe-assembly assembly { result := mload(0x40) @@ -113,8 +91,7 @@ contract MockAccountFallback is IAccount { } fallback(bytes calldata callData) external payable returns (bytes memory) { - (bool success, bytes memory result) = - address(handler).call{value: msg.value}(ERC2771Lib.get2771CallData(callData)); + (bool success, bytes memory result) = address(handler).call{ value: msg.value }(ERC2771Lib.get2771CallData(callData)); if (!success) { revert FallbackFailed(result); } diff --git a/test/mock/MockAccountNonRevert.sol b/test/mock/MockAccountNonRevert.sol index cbb05d7..26e343f 100644 --- a/test/mock/MockAccountNonRevert.sol +++ b/test/mock/MockAccountNonRevert.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {IAccount} from "account-abstraction/interfaces/IAccount.sol"; -import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol"; -import {IValidator, IFallback} from "erc7579/interfaces/IERC7579Module.sol"; -import {IStatelessValidator} from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; -import {EIP1271_SUCCESS, EIP1271_FAILED} from "contracts/types/Constants.sol"; -import {ERC2771Lib} from "./lib/ERC2771Lib.sol"; -import {ComposableExecutionBase} from "contracts/ComposableExecutionBase.sol"; -import {ComposableExecution} from "contracts/types/ComposabilityDataTypes.sol"; +import { IAccount } from "account-abstraction/interfaces/IAccount.sol"; +import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol"; +import { IValidator, IFallback } from "erc7579/interfaces/IERC7579Module.sol"; +import { IStatelessValidator } from "node_modules/@rhinestone/module-bases/src/interfaces/IStatelessValidator.sol"; +import { EIP1271_SUCCESS, EIP1271_FAILED } from "contracts/types/Constants.sol"; +import { ERC2771Lib } from "./lib/ERC2771Lib.sol"; +import { ComposableExecutionBase } from "contracts/ComposableExecutionBase.sol"; +import { ComposableExecution } from "contracts/types/ComposabilityDataTypes.sol"; -import {console2} from "forge-std/console2.sol"; +import { console2 } from "forge-std/console2.sol"; address constant ENTRY_POINT_V07 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; @@ -30,10 +30,7 @@ contract MockAccountNonRevert is ComposableExecutionBase, IAccount { handler = IFallback(_handler); } - function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) - external - returns (uint256 vd) - { + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external returns (uint256 vd) { if (address(validator) != address(0)) { vd = validator.validateUserOp(userOp, userOpHash); } @@ -41,28 +38,16 @@ contract MockAccountNonRevert is ComposableExecutionBase, IAccount { } function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { - return - IValidator(address(validator)).isValidSignatureWithSender({sender: msg.sender, hash: hash, data: signature}); + return IValidator(address(validator)).isValidSignatureWithSender({ sender: msg.sender, hash: hash, data: signature }); } - function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) - external - view - returns (bool) - { - return IStatelessValidator(address(validator)).validateSignatureWithData({ - hash: signedHash, - signature: signature, - data: signerData - }); + function validateSignatureWithData(bytes32 signedHash, bytes calldata signature, bytes calldata signerData) external view returns (bool) { + return IStatelessValidator(address(validator)).validateSignatureWithData({ hash: signedHash, signature: signature, data: signerData }); } - function execute(address to, uint256 value, bytes calldata data) - external - returns (bool success, bytes memory result) - { + function execute(address to, uint256 value, bytes calldata data) external returns (bool success, bytes memory result) { emit MockAccountExecute(to, value, data); - (success, result) = to.call{value: value}(data); + (success, result) = to.call{ value: value }(data); } function executeComposable(ComposableExecution[] calldata cExecutions) external payable override { @@ -70,12 +55,8 @@ contract MockAccountNonRevert is ComposableExecutionBase, IAccount { _executeComposable(cExecutions); } - function _executeAction(address to, uint256 value, bytes memory data) - internal - override - returns (bytes memory returnData) - { - (, returnData) = to.call{value: value}(data); + function _executeAction(address to, uint256 value, bytes memory data) internal override returns (bytes memory returnData) { + (, returnData) = to.call{ value: value }(data); } receive() external payable { @@ -83,8 +64,7 @@ contract MockAccountNonRevert is ComposableExecutionBase, IAccount { } fallback(bytes calldata callData) external payable returns (bytes memory) { - (bool success, bytes memory result) = - address(handler).call{value: msg.value}(ERC2771Lib.get2771CallData(callData)); + (bool success, bytes memory result) = address(handler).call{ value: msg.value }(ERC2771Lib.get2771CallData(callData)); if (!success) { revert(string(result)); } diff --git a/test/mock/MockERC20Balance.sol b/test/mock/MockERC20Balance.sol index 282b2a4..be885e2 100644 --- a/test/mock/MockERC20Balance.sol +++ b/test/mock/MockERC20Balance.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.23; contract MockERC20Balance { - mapping(address => uint256) public balanceOf; - + function setBalance(address account, uint256 balance) public { balanceOf[account] = balance; } -} \ No newline at end of file +} diff --git a/test/unit/ComposableExecution_Complex.sol b/test/unit/ComposableExecution_Complex.sol index 561c647..9e91419 100644 --- a/test/unit/ComposableExecution_Complex.sol +++ b/test/unit/ComposableExecution_Complex.sol @@ -3,13 +3,12 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "test/ComposabilityBase.t.sol"; -import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; -import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import { ComposableExecutionModule } from "contracts/ComposableExecutionModule.sol"; +import { IComposableExecution } from "contracts/interfaces/IComposableExecution.sol"; import "contracts/ComposableExecutionLib.sol"; import "contracts/types/ComposabilityDataTypes.sol"; contract ComposableExecutionTestComplexCases is ComposabilityTestBase { - function setUp() public override { super.setUp(); } @@ -48,7 +47,7 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { _structInjection(address(mockAccountCaller), address(composabilityHandler)); _structInjection(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); } - + // ================================================================================= // ================================ TEST SCENARIOS ================================ // ================================================================================= @@ -59,19 +58,12 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { InputParam[] memory inputParams = new InputParam[](2); inputParams[0] = _createRawTargetInputParam(address(dummyContract)); inputParams[1] = _createRawValueInputParam(0); - + OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(4, address(storageContract), SLOT_A) - }); + outputParams[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(4, address(storageContract), SLOT_A) }); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.returnMultipleValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.returnMultipleValues.selector, inputParams: inputParams, outputParams: outputParams }); IComposableExecution(address(account)).executeComposable(executions); vm.stopPrank(); @@ -133,22 +125,10 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { // test inputStaticCall with multiple return values function _inputStaticCallMultipleValues(address account, address caller) internal { Constraint[] memory constraints = new Constraint[](4); - constraints[0] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(2517))) - }); - constraints[1] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(uint160(address(dummyContract))))) - }); - constraints[2] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(keccak256("DUMMY")))) - }); - constraints[3] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(1))) - }); + constraints[0] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(2517))) }); + constraints[1] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(uint160(address(dummyContract))))) }); + constraints[2] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(keccak256("DUMMY")))) }); + constraints[3] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(1))) }); vm.startPrank(ENTRYPOINT_V07_ADDRESS); @@ -165,11 +145,7 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.acceptMultipleValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.acceptMultipleValues.selector, inputParams: inputParams, outputParams: outputParams }); vm.expectEmit(address(dummyContract)); emit Uint256Emitted(2517); @@ -188,11 +164,11 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { vm.startPrank(ENTRYPOINT_V07_ADDRESS); uint256 someStaticValue = 2517; - uint256 expectedUint256 = 2517*2; + uint256 expectedUint256 = 2517 * 2; bytes memory expectedBytes = bytes("Hello, world!"); address expectedAddress = address(0xa11cedecaf); - // encode function call as per https://docs.soliditylang.org/en/develop/abi-spec.html + // encode function call as per https://docs.soliditylang.org/en/develop/abi-spec.html // function is : function acceptStaticAndDynamicValues(uint256 staticValue, bytes calldata dynamicValue, address addr) // static arg @@ -221,7 +197,7 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { paramData: abi.encode(expectedAddress), constraints: emptyConstraints }); - + // the payload of the dynamic arg inputParams[5] = InputParam({ paramType: InputParamType.CALL_DATA, @@ -234,11 +210,8 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.acceptStaticAndDynamicValues.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = + ComposableExecution({ functionSig: DummyContract.acceptStaticAndDynamicValues.selector, inputParams: inputParams, outputParams: outputParams }); vm.expectEmit(address(dummyContract)); emit Uint256Emitted(expectedUint256); @@ -263,12 +236,9 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { uint256 fee = 500; Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.LTE, - referenceData: abi.encode(bytes32(uint256(10_000))) - }); + constraints[0] = Constraint({ constraintType: ConstraintType.LTE, referenceData: abi.encode(bytes32(uint256(10_000))) }); - // represent the encoded call to acceptStruct() + // represent the encoded call to acceptStruct() // as per abi encoding rules InputParam[] memory inputParams = new InputParam[](9); @@ -318,7 +288,6 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { constraints: emptyConstraints }); - // deadline inputParams[7] = InputParam({ paramType: InputParamType.CALL_DATA, @@ -340,16 +309,12 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.acceptStruct.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.acceptStruct.selector, inputParams: inputParams, outputParams: outputParams }); vm.expectEmit(address(dummyContract)); emit Uint256Emitted(someStaticValue); // someValue vm.expectEmit(address(dummyContract)); - emit Uint256Emitted(someStaticValue*2); //amountIn + emit Uint256Emitted(someStaticValue * 2); //amountIn vm.expectEmit(address(dummyContract)); emit Uint256Emitted(amountOutMin); //amountOutMin vm.expectEmit(address(dummyContract)); @@ -365,4 +330,3 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase { vm.stopPrank(); } } - diff --git a/test/unit/ComposableExecution_ConstraintsReverts.sol b/test/unit/ComposableExecution_ConstraintsReverts.sol index 739e47d..0c51d4d 100644 --- a/test/unit/ComposableExecution_ConstraintsReverts.sol +++ b/test/unit/ComposableExecution_ConstraintsReverts.sol @@ -3,13 +3,12 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "test/ComposabilityBase.t.sol"; -import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; -import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import { ComposableExecutionModule } from "contracts/ComposableExecutionModule.sol"; +import { IComposableExecution } from "contracts/interfaces/IComposableExecution.sol"; import "contracts/ComposableExecutionLib.sol"; import "contracts/types/ComposabilityDataTypes.sol"; contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { - error FallbackFailed(bytes result); error InvalidParameterEncoding(string message); @@ -18,7 +17,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { } function test_inputs_With_Gte_Constraints() public { - _inputParamUsingGteConstraints(address(mockAccount), address(mockAccount)); + _inputParamUsingGteConstraints(address(mockAccount), address(mockAccount)); _inputParamUsingGteConstraints(address(mockAccountFallback), address(composabilityHandler)); _inputParamUsingGteConstraints(address(mockAccountCaller), address(composabilityHandler)); _inputParamUsingGteConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); @@ -43,15 +42,15 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { _inputParamUsingEqConstraints(address(mockAccountFallback), address(composabilityHandler)); _inputParamUsingEqConstraints(address(mockAccountCaller), address(composabilityHandler)); _inputParamUsingEqConstraints(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); - } + } function test_read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized() public { - _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountFallback), address(composabilityHandler)); + _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountFallback), address(composabilityHandler)); _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccount), address(mockAccount)); _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountCaller), address(composabilityHandler)); _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); } - + // if the account does not revert on unsuccessful execution, // the revert reason is saved in the storage function test_save_Revert_Reason_in_Storage() public { @@ -64,17 +63,14 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccountCaller), address(composabilityHandler)); _balance_Fetcher_Reverts_If_Used_For_TARGET_Param(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); } - + // ================================================================================= // ================================ TEST SCENARIOS ================================ // ================================================================================= function _inputParamUsingGteConstraints(address account, address caller) internal { Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.GTE, - referenceData: abi.encode(bytes32(uint256(43))) - }); + constraints[0] = Constraint({ constraintType: ConstraintType.GTE, referenceData: abi.encode(bytes32(uint256(43))) }); vm.startPrank(ENTRYPOINT_V07_ADDRESS); @@ -109,9 +105,11 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { inputParams: invalidInputParams, // use constrainted input parameter that's going to fail outputParams: outputParams }); - bytes memory expectedRevertData; + bytes memory expectedRevertData; if (address(account) == address(mockAccountFallback)) { - expectedRevertData = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE)); + expectedRevertData = abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE) + ); } else { expectedRevertData = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.GTE); } @@ -130,11 +128,8 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { function _inputParamUsingLteConstraints(address account, address caller) internal { Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.LTE, - referenceData: abi.encode(bytes32(uint256(41))) - }); - + constraints[0] = Constraint({ constraintType: ConstraintType.LTE, referenceData: abi.encode(bytes32(uint256(41))) }); + vm.startPrank(ENTRYPOINT_V07_ADDRESS); // Prepare invalid input param - call should revert @@ -170,9 +165,11 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { inputParams: invalidInputParams, // use constrainted input parameter that's going to fail outputParams: outputParams }); - bytes memory expectedRevertReason; + bytes memory expectedRevertReason; if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE)); + expectedRevertReason = abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE) + ); } else { expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.LTE); } @@ -191,10 +188,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { function _inputParamUsingInConstraints(address account, address caller) internal { Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.IN, - referenceData: abi.encode(bytes32(uint256(41)), bytes32(uint256(43))) - }); + constraints[0] = Constraint({ constraintType: ConstraintType.IN, referenceData: abi.encode(bytes32(uint256(41)), bytes32(uint256(43))) }); vm.startPrank(ENTRYPOINT_V07_ADDRESS); @@ -243,9 +237,11 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { inputParams: invalidInputParamsA, // use constrainted input parameter that's going to fail outputParams: outputParams }); - bytes memory expectedRevertReason; + bytes memory expectedRevertReason; if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); + expectedRevertReason = abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN) + ); } else { expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); } @@ -259,9 +255,11 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { inputParams: invalidInputParamsB, // use constrainted input parameter that's going to fail outputParams: outputParams }); - + if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN)); + expectedRevertReason = abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN) + ); } else { expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.IN); } @@ -280,10 +278,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { function _inputParamUsingEqConstraints(address account, address caller) internal { Constraint[] memory constraints = new Constraint[](1); - constraints[0] = Constraint({ - constraintType: ConstraintType.EQ, - referenceData: abi.encode(bytes32(uint256(42))) - }); + constraints[0] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(42))) }); vm.startPrank(ENTRYPOINT_V07_ADDRESS); @@ -297,7 +292,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { constraints: constraints }); invalidInputParams[1] = _createRawTargetInputParam(address(0)); - invalidInputParams[2] = _createRawValueInputParam(0); + invalidInputParams[2] = _createRawValueInputParam(0); // Prepare valid input param - call should succeed InputParam[] memory validInputParams = new InputParam[](3); @@ -320,9 +315,11 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { inputParams: invalidInputParams, // use constrainted input parameter that's going to fail outputParams: outputParams }); - bytes memory expectedRevertReason; + bytes memory expectedRevertReason; if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ)); + expectedRevertReason = abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ) + ); } else { expectedRevertReason = abi.encodeWithSelector(ComposableExecutionLib.ConstraintNotMet.selector, ConstraintType.EQ); } @@ -339,9 +336,9 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { IComposableExecution(address(account)).executeComposable(validExecutions); } - // It can happen when the previous call, that creates the output params, fail. + // It can happen when the previous call, that creates the output params, fail. // In this case, the composable execution should revert when reading this from storage - function _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address account, address caller) internal { + function _read_From_Storage_Reverts_if_the_expected_slot_is_not_initialized(address account, address caller) internal { vm.startPrank(ENTRYPOINT_V07_ADDRESS); bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); @@ -361,15 +358,12 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.B.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.B.selector, inputParams: inputParams, outputParams: outputParams }); - bytes memory expectedRevertReason; + bytes memory expectedRevertReason; if (address(account) == address(mockAccountFallback)) { - expectedRevertReason = abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector)); + expectedRevertReason = + abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector)); } else { expectedRevertReason = abi.encodePacked(ComposableExecutionLib.ComposableExecutionFailed.selector); } @@ -396,28 +390,18 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { }); OutputParam[] memory outputParamsExecA = new OutputParam[](1); - outputParamsExecA[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode( - 1, - address(storageContract), - SLOT_B - ) - }); + outputParamsExecA[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, address(storageContract), SLOT_B) }); ComposableExecution[] memory executionsA = new ComposableExecution[](1); - executionsA[0] = ComposableExecution({ - functionSig: DummyContract.revertWithReason.selector, - inputParams: inputParamsExecA, - outputParams: outputParamsExecA - }); + executionsA[0] = + ComposableExecution({ functionSig: DummyContract.revertWithReason.selector, inputParams: inputParamsExecA, outputParams: outputParamsExecA }); IComposableExecution(address(account)).executeComposable(executionsA); bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); bytes32 SLOT_B_0 = keccak256(abi.encodePacked(SLOT_B, uint256(0))); bytes32 storedValue0 = storageContract.readStorage(namespace, SLOT_B_0); - + bytes32 expectedValue = bytes32(DummyRevert.selector); assertEq(storedValue0, expectedValue, "Value 0 not stored correctly in the composability storage"); @@ -428,7 +412,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { vm.startPrank(ENTRYPOINT_V07_ADDRESS); InputParam[] memory inputParams = new InputParam[](2); - + inputParams[0] = _createRawValueInputParam(0); inputParams[1] = InputParam({ @@ -441,14 +425,15 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: "", - inputParams: inputParams, - outputParams: outputParams - }); - + executions[0] = ComposableExecution({ functionSig: "", inputParams: inputParams, outputParams: outputParams }); + if (address(account) == address(mockAccountFallback)) { - vm.expectRevert(abi.encodeWithSelector(MockAccountFallback.FallbackFailed.selector, abi.encodeWithSelector(InvalidParameterEncoding.selector, "BALANCE fetcher type is not supported for TARGET param type"))); + vm.expectRevert( + abi.encodeWithSelector( + MockAccountFallback.FallbackFailed.selector, + abi.encodeWithSelector(InvalidParameterEncoding.selector, "BALANCE fetcher type is not supported for TARGET param type") + ) + ); } else { vm.expectRevert(abi.encodeWithSelector(InvalidParameterEncoding.selector, "BALANCE fetcher type is not supported for TARGET param type")); } @@ -457,4 +442,3 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { vm.stopPrank(); } } - diff --git a/test/unit/ComposableExecution_Simple.t.sol b/test/unit/ComposableExecution_Simple.t.sol index a737c60..8784b16 100644 --- a/test/unit/ComposableExecution_Simple.t.sol +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -3,13 +3,12 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "test/ComposabilityBase.t.sol"; -import {ComposableExecutionModule} from "contracts/ComposableExecutionModule.sol"; -import {IComposableExecution} from "contracts/interfaces/IComposableExecution.sol"; +import { ComposableExecutionModule } from "contracts/ComposableExecutionModule.sol"; +import { IComposableExecution } from "contracts/interfaces/IComposableExecution.sol"; import "contracts/ComposableExecutionLib.sol"; import "contracts/types/ComposabilityDataTypes.sol"; contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { - function setUp() public override { super.setUp(); } @@ -22,14 +21,14 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { _inputStaticCallOutputExecResult(address(mockAccount), address(mockAccount)); // via regular call - _inputStaticCallOutputExecResult(address(mockAccountCaller), address(composabilityHandler)); + _inputStaticCallOutputExecResult(address(mockAccountCaller), address(composabilityHandler)); // via delegatecall _inputStaticCallOutputExecResult(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); } function test_inputRawBytes_Success() public { - _inputRawBytes(address(mockAccountFallback), address(composabilityHandler)); + _inputRawBytes(address(mockAccountFallback), address(composabilityHandler)); _inputRawBytes(address(mockAccount), address(mockAccount)); _inputRawBytes(address(mockAccountCaller), address(composabilityHandler)); _inputRawBytes(address(mockAccountDelegateCaller), address(mockAccountDelegateCaller)); @@ -106,17 +105,14 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { inputParamsA[1] = _createRawValueInputParam(0); OutputParam[] memory outputParamsA = new OutputParam[](1); - outputParamsA[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, storageContract, SLOT_A) - }); + outputParamsA[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, storageContract, SLOT_A) }); ComposableExecution[] memory executions = new ComposableExecution[](1); executions[0] = ComposableExecution({ functionSig: DummyContract.A.selector, inputParams: inputParamsA, // TARGET and VALUE parameters only outputParams: outputParamsA // store output of the function A() to the storage - }); + }); // Call function A IComposableExecution(address(account)).executeComposable(executions); @@ -140,17 +136,10 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { // Prepare return value config for function B OutputParam[] memory outputParamsB = new OutputParam[](1); - outputParamsB[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, storageContract, SLOT_B) - }); + outputParamsB[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, storageContract, SLOT_B) }); ComposableExecution[] memory executionsB = new ComposableExecution[](1); - executionsB[0] = ComposableExecution({ - functionSig: DummyContract.B.selector, - inputParams: inputParamsB, - outputParams: outputParamsB - }); + executionsB[0] = ComposableExecution({ functionSig: DummyContract.B.selector, inputParams: inputParamsB, outputParams: outputParamsB }); // Call function B IComposableExecution(address(account)).executeComposable(executionsB); @@ -187,11 +176,7 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.emitUint256.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.emitUint256.selector, inputParams: inputParams, outputParams: outputParams }); vm.expectEmit(address(dummyContract)); emit Uint256Emitted(1); @@ -218,21 +203,11 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](1); outputParams[0] = OutputParam({ fetcherType: OutputParamFetcherType.STATIC_CALL, - paramData: abi.encode( - 1, - address(dummyContract), - abi.encodeWithSelector(DummyContract.getFoo.selector), - address(storageContract), - SLOT_B - ) + paramData: abi.encode(1, address(dummyContract), abi.encodeWithSelector(DummyContract.getFoo.selector), address(storageContract), SLOT_B) }); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.getFoo.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.getFoo.selector, inputParams: inputParams, outputParams: outputParams }); uint256 expectedValue = 2517; dummyContract.setFoo(expectedValue); @@ -273,19 +248,11 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { }); OutputParam[] memory outputParams_execution1 = new OutputParam[](2); - outputParams_execution1[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); + outputParams_execution1[0] = + OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, address(storageContract), SLOT_A) }); outputParams_execution1[1] = OutputParam({ fetcherType: OutputParamFetcherType.STATIC_CALL, - paramData: abi.encode( - 1, - address(dummyContract), - abi.encodeWithSelector(DummyContract.getFoo.selector), - address(storageContract), - SLOT_B - ) + paramData: abi.encode(1, address(dummyContract), abi.encodeWithSelector(DummyContract.getFoo.selector), address(storageContract), SLOT_B) }); bytes32 namespace = storageContract.getNamespace(address(account), address(caller)); @@ -311,16 +278,10 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams_execution2 = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](2); - executions[0] = ComposableExecution({ - functionSig: DummyContract.swap.selector, - inputParams: inputParams_execution1, - outputParams: outputParams_execution1 - }); - executions[1] = ComposableExecution({ - functionSig: DummyContract.stake.selector, - inputParams: inputParams_execution2, - outputParams: outputParams_execution2 - }); + executions[0] = + ComposableExecution({ functionSig: DummyContract.swap.selector, inputParams: inputParams_execution1, outputParams: outputParams_execution1 }); + executions[1] = + ComposableExecution({ functionSig: DummyContract.stake.selector, inputParams: inputParams_execution2, outputParams: outputParams_execution2 }); uint256 expectedToStake = input1 + 1; uint256 messageValue; @@ -340,11 +301,11 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { // stake emits input params: first param is from swap, second param is from getFoo which is just input1 vm.expectEmit(address(dummyContract)); emit Uint256Emitted2(expectedToStake, input1); - + vm.expectEmit(address(dummyContract)); emit Received(valueToSend); - IComposableExecution(address(account)).executeComposable{value: messageValue}(executions); + IComposableExecution(address(account)).executeComposable{ value: messageValue }(executions); //check storage slots bytes32 storedValueA = storageContract.readStorage(namespace, SLOT_A_0); @@ -367,17 +328,10 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { inputParams[1] = _createRawValueInputParam(0); OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); + outputParams[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, address(storageContract), SLOT_A) }); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.getAddress.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.getAddress.selector, inputParams: inputParams, outputParams: outputParams }); IComposableExecution(address(account)).executeComposable(executions); vm.stopPrank(); @@ -396,17 +350,10 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { inputParams[1] = _createRawValueInputParam(0); OutputParam[] memory outputParams = new OutputParam[](1); - outputParams[0] = OutputParam({ - fetcherType: OutputParamFetcherType.EXEC_RESULT, - paramData: abi.encode(1, address(storageContract), SLOT_A) - }); + outputParams[0] = OutputParam({ fetcherType: OutputParamFetcherType.EXEC_RESULT, paramData: abi.encode(1, address(storageContract), SLOT_A) }); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.getBool.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.getBool.selector, inputParams: inputParams, outputParams: outputParams }); IComposableExecution(address(account)).executeComposable(executions); vm.stopPrank(); @@ -418,7 +365,6 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { } function _runtime_Value_Injection(address account, address caller) internal { - vm.startPrank(ENTRYPOINT_V07_ADDRESS); InputParam[] memory inputParams = new InputParam[](2); @@ -434,24 +380,20 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.payableEmit.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.payableEmit.selector, inputParams: inputParams, outputParams: outputParams }); uint256 expectedValue = dummyContract.getNativeValue(); vm.expectEmit(address(dummyContract)); emit Received(expectedValue); IComposableExecution(address(account)).executeComposable(executions); - + vm.stopPrank(); } function _runtime_Target_Injection(address account, address caller) internal { vm.startPrank(ENTRYPOINT_V07_ADDRESS); - uint256 uintToEmit = 1823920923; + uint256 uintToEmit = 1_823_920_923; InputParam[] memory inputParams = new InputParam[](3); inputParams[0] = InputParam({ @@ -471,23 +413,19 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.emitUint256.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.emitUint256.selector, inputParams: inputParams, outputParams: outputParams }); vm.expectEmit(address(dummyContract)); emit Uint256Emitted(uintToEmit); IComposableExecution(address(account)).executeComposable(executions); - + vm.stopPrank(); } function _erc20_Balance_Fetcher(address account, address caller) internal { vm.startPrank(ENTRYPOINT_V07_ADDRESS); - uint256 balanceToSet = 139122330912355; + uint256 balanceToSet = 139_122_330_912_355; mockERC20Balance.setBalance(address(0xa11ce), balanceToSet); InputParam[] memory inputParams = new InputParam[](2); @@ -499,18 +437,14 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { constraints: emptyConstraints }); - // since this is commented out, this test case also + // since this is commented out, this test case also // makes sure that the value = 0 is used if the VALUE param is not provided // inputParams[2] = _createRawValueInputParam(0); OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.emitUint256.selector, - inputParams: inputParams, - outputParams: outputParams - }); + executions[0] = ComposableExecution({ functionSig: DummyContract.emitUint256.selector, inputParams: inputParams, outputParams: outputParams }); // balance is used as param to the emitUint256 function vm.expectEmit(address(dummyContract)); @@ -523,14 +457,14 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { function _native_Balance_Fetcher(address account, address caller) internal { vm.startPrank(ENTRYPOINT_V07_ADDRESS); - uint256 balanceToSet = 139122330912355; + uint256 balanceToSet = 139_122_330_912_355; vm.deal(address(0xa11ce), balanceToSet); assertEq(address(0xa11ce).balance, balanceToSet); InputParam[] memory inputParams = new InputParam[](3); inputParams[0] = _createRawTargetInputParam(address(dummyContract)); inputParams[1] = _createRawValueInputParam(0); - + inputParams[2] = InputParam({ paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.BALANCE, @@ -541,21 +475,14 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { OutputParam[] memory outputParams = new OutputParam[](0); ComposableExecution[] memory executions = new ComposableExecution[](1); - executions[0] = ComposableExecution({ - functionSig: DummyContract.emitUint256.selector, - inputParams: inputParams, - outputParams: outputParams - }); - + executions[0] = ComposableExecution({ functionSig: DummyContract.emitUint256.selector, inputParams: inputParams, outputParams: outputParams }); + vm.expectEmit(address(dummyContract)); emit Uint256Emitted(balanceToSet); IComposableExecution(address(account)).executeComposable(executions); vm.stopPrank(); } - - - /* @@ -565,4 +492,4 @@ contract ComposableExecutionTestSimpleCases is ComposabilityTestBase { */ -} \ No newline at end of file +} From ed0b8d092a49b11aa6a9ed54291c450e16c9fce7 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 9 Oct 2025 15:42:29 +0300 Subject: [PATCH 10/11] chore: clean and comment --- contracts/ComposableExecutionLib.sol | 11 ++++++----- test/unit/ComposableExecution_ConstraintsReverts.sol | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index 23f3eae..fad226c 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -58,12 +58,13 @@ library ComposableExecutionLib { revert InvalidParameterEncoding("Invalid param type"); } } - if (composedTarget == address(0)) { - revert InvalidSetOfInputParams("TARGET InputParam is required"); - } + // if a param with TARGET type was not provided, it will be address(0) + // we don't restrict it since some calls may want to call address(0) + // if a param with VALUE type was not provided, it will be 0 + // this is even more often case, as many calls happen with 0 value return Execution({ - target: composedTarget, - value: composedValue, // if a param with VALUE type was not provided, it will be 0 + target: composedTarget, + value: composedValue, callData: composedCalldata }); } diff --git a/test/unit/ComposableExecution_ConstraintsReverts.sol b/test/unit/ComposableExecution_ConstraintsReverts.sol index 0c51d4d..2d312a1 100644 --- a/test/unit/ComposableExecution_ConstraintsReverts.sol +++ b/test/unit/ComposableExecution_ConstraintsReverts.sol @@ -287,8 +287,7 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { invalidInputParams[0] = InputParam({ paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, - paramData: abi.encode(43), - //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 + paramData: abi.encode(43), // value must be exactly 42 constraints: constraints }); invalidInputParams[1] = _createRawTargetInputParam(address(0)); @@ -300,7 +299,6 @@ contract ComposableExecutionTestConstraintsAndReverts is ComposabilityTestBase { paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(42), - //constraints: abi.encodePacked(ConstraintType.EQ, bytes32(uint256(42))) // value must be exactly 42 constraints: constraints }); validInputParams[1] = _createRawTargetInputParam(address(0)); From 5ec5d4759196d83b0a233a0d64a2c74241e08270 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Tue, 21 Oct 2025 11:34:49 +0300 Subject: [PATCH 11/11] fix add length check --- contracts/ComposableExecutionLib.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index fad226c..ed13823 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -96,11 +96,16 @@ library ComposableExecutionLib { address tokenAddr; address account; bytes calldata paramData = param.paramData; + // expect paramData to be abi.encodePacked(address token, address account) + // Validate exact length requirement + require(paramData.length == 40, + InvalidParameterEncoding("Invalid paramData length")); assembly { tokenAddr := shr(96, calldataload(paramData.offset)) account := shr(96, calldataload(add(paramData.offset, 0x14))) } + uint256 balance; if (tokenAddr == address(0)) { balance = account.balance;