diff --git a/contracts/ComposableExecutionBase.sol b/contracts/ComposableExecutionBase.sol index 230986c..48c0ddd 100644 --- a/contracts/ComposableExecutionBase.sol +++ b/contracts/ComposableExecutionBase.sol @@ -1,40 +1,51 @@ // 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"; -import {IComposableExecution} from "./interfaces/IComposableExecution.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[]; 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)); + // 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)); } } /// @dev Override this in the account /// using account's native execution approach - function _executeAction(address to, uint256 value, bytes memory data) - internal - virtual - returns (bytes memory returnData); + /// 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); } diff --git a/contracts/ComposableExecutionLib.sol b/contracts/ComposableExecutionLib.sol index ba63232..ed13823 100644 --- a/contracts/ComposableExecutionLib.sol +++ b/contracts/ComposableExecutionLib.sol @@ -1,31 +1,72 @@ // 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 { 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(); + 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 (bytes 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++) { - composedCalldata = bytes.concat(composedCalldata, processInput(inputParams[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"); + } + // 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); + } else { + revert InvalidParameterEncoding("Invalid param type"); + } } - return composedCalldata; + // 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, + callData: composedCalldata + }); } // Process a single input parameter and return the composed calldata @@ -37,6 +78,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)) @@ -50,8 +92,30 @@ library ComposableExecutionLib { } _validateConstraints(returnData, param.constraints); return returnData; + } else if (param.fetcherType == InputParamFetcherType.BALANCE) { + 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; + } else { + balance = IERC20(tokenAddr).balanceOf(account); + } + _validateConstraints(abi.encode(balance), param.constraints); + return abi.encode(balance); } else { - revert InvalidParameterEncoding(); + revert InvalidParameterEncoding("Invalid param fetcher type"); } } @@ -78,7 +142,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; @@ -107,10 +171,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]; @@ -135,17 +196,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 64e2ca5..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} 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; @@ -47,69 +54,69 @@ 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 // 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(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 - ) internal { + 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); - bytes[] memory returnData; - if (execution.to != address(0)) { - returnData = executeExecutionFunction(execution, composedCalldata); + ComposableExecution calldata cExecution = cExecutions[i]; + Execution memory execution = cExecution.inputParams.processInputs(cExecution.functionSig); + bytes[] memory returnData; + 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) - }); + 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 - 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 @@ -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 fb64637..51da00f 100644 --- a/contracts/interfaces/IComposableExecution.sol +++ b/contracts/interfaces/IComposableExecution.sol @@ -1,13 +1,13 @@ // 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 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..e3e964f 100644 --- a/contracts/types/ComposabilityDataTypes.sol +++ b/contracts/types/ComposabilityDataTypes.sol @@ -1,16 +1,26 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; +pragma solidity ^0.8.23; + +// 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 { EXEC_RESULT, // The return of the execution call STATIC_CALL // Call to some other function + } // Constraint type for parameter validation @@ -19,6 +29,7 @@ enum ConstraintType { GTE, // Greater than or equal to LTE, // Less than or equal to IN // In range + } // Constraint for parameter validation @@ -29,6 +40,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 +54,6 @@ struct OutputParam { // Structure to define a composable execution struct ComposableExecution { - address to; - uint256 value; bytes4 functionSig; InputParam[] inputParams; OutputParam[] outputParams; 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..ad2deac 100644 --- a/test/ComposabilityBase.t.sol +++ b/test/ComposabilityBase.t.sol @@ -1,13 +1,17 @@ // 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 { 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"; address constant ENTRYPOINT_V07_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032; @@ -18,28 +22,31 @@ contract ComposabilityTestBase is Test { MockAccountCaller internal mockAccountCaller; MockAccountNonRevert internal mockAccountNonRevert; MockAccount internal mockAccount; + MockERC20Balance internal mockERC20Balance; + + 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({ - _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 vm.deal(address(mockAccountFallback), 100 ether); @@ -48,5 +55,27 @@ 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..9999d28 100644 --- a/test/mock/DummyContract.sol +++ b/test/mock/DummyContract.sol @@ -1,13 +1,19 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.27; +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) { @@ -34,6 +39,10 @@ contract DummyContract { return value * 2; } + function getNativeValue() external pure returns (uint256) { + return 10_491; // 10491 wei + } + function getFoo() external view returns (uint256) { return foo; } @@ -97,4 +106,8 @@ contract DummyContract { function revertWithReason(uint256 value) external pure { revert DummyRevert(value); } -} \ No newline at end of file + + function payableEmit() external payable { + emit Received(msg.value); + } +} diff --git a/test/mock/MockAccount.sol b/test/mock/MockAccount.sol index e6478a9..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,42 +39,26 @@ 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 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) - 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 24fc6ad..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,7 +20,7 @@ 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; @@ -37,14 +37,11 @@ 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) - 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); } @@ -52,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(); @@ -94,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 a141632..c84e920 100644 --- a/test/mock/MockAccountDelegateCaller.sol +++ b/test/mock/MockAccountDelegateCaller.sol @@ -4,22 +4,21 @@ pragma solidity ^0.8.23; import "contracts/interfaces/IComposableExecution.sol"; contract MockAccountDelegateCaller { - address composableModule; + event MockAccountDelegateCall(bytes returnData); constructor(address _composableModule) { 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) { - 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 d11aa2b..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,41 +38,25 @@ 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 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) - 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 new file mode 100644 index 0000000..be885e2 --- /dev/null +++ b/test/mock/MockERC20Balance.sol @@ -0,0 +1,10 @@ +// 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; + } +} 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..9e91419 --- /dev/null +++ b/test/unit/ComposableExecution_Complex.sol @@ -0,0 +1,332 @@ +// 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..2d312a1 --- /dev/null +++ b/test/unit/ComposableExecution_ConstraintsReverts.sol @@ -0,0 +1,442 @@ +// 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 { + error FallbackFailed(bytes result); + error InvalidParameterEncoding(string message); + + 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)); + } + + 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 ================================ + // ================================================================================= + + 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), // 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: 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(); + } + + 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 new file mode 100644 index 0000000..8784b16 --- /dev/null +++ b/test/unit/ComposableExecution_Simple.t.sol @@ -0,0 +1,495 @@ +// 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)); + } + + 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 ================================ + // ================================================================================= + + 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 = 1_823_920_923; + + 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(); + } + + function _erc20_Balance_Fetcher(address account, address caller) internal { + vm.startPrank(ENTRYPOINT_V07_ADDRESS); + + uint256 balanceToSet = 139_122_330_912_355; + mockERC20Balance.setBalance(address(0xa11ce), balanceToSet); + + InputParam[] memory inputParams = new InputParam[](2); + inputParams[0] = _createRawTargetInputParam(address(dummyContract)); + inputParams[1] = InputParam({ + paramType: InputParamType.CALL_DATA, + fetcherType: InputParamFetcherType.BALANCE, + paramData: abi.encodePacked(address(mockERC20Balance), address(0xa11ce)), + constraints: emptyConstraints + }); + + // 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 }); + + // 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 = 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, + 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(); + } + + /* + + runtime address injection + + test fetcher type BALANCE + + + */ +}