Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install Node dependencies
run: npm install --ignore-scripts

- name: Show Forge version
run: |
forge --version
Expand Down
94 changes: 68 additions & 26 deletions contracts/ComposableExecutionLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ library ComposableExecutionLib {
// 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
});
return Execution({ target: composedTarget, value: composedValue, callData: composedCalldata });
}

// Process a single input parameter and return the composed calldata
Expand Down Expand Up @@ -99,8 +95,7 @@ library ComposableExecutionLib {

// expect paramData to be abi.encodePacked(address token, address account)
// Validate exact length requirement
require(paramData.length == 40,
InvalidParameterEncoding("Invalid paramData length"));
require(paramData.length == 40, InvalidParameterEncoding("Invalid paramData length"));
assembly {
tokenAddr := shr(96, calldataload(paramData.offset))
account := shr(96, calldataload(add(paramData.offset, 0x14)))
Expand Down Expand Up @@ -170,28 +165,75 @@ library ComposableExecutionLib {
}
}

/// @dev Validate the constraints => compare the value with the reference data
/// @dev Validate the constraints => compare each 32-byte word of rawValue against constraints[i].
/// Each constraints[i] is checked against the i-th 32-byte word of rawValue (AND semantics across
/// the array). Use ConstraintType.OR to express OR semantics within a single word position.
///
/// AND vs OR encoding:
/// - AND is implicit across the top-level `Constraint[]`. Every non-OR entry has a static
/// 32-byte reference (EQ/GTE/LTE/GTE_SIGNED/LTE_SIGNED) or a fixed 64-byte (lower, upper)
/// payload (IN) — predictable layout, predictable gas.
/// - OR is a single entry whose `referenceData` is a dynamic `abi.encode(Constraint[])`. It is
/// decoded once here and evaluated against the *same* 32-byte word as the outer entry. The
/// sub-constraints inside the OR must be leaf constraints only — nesting OR inside OR is
/// intentionally rejected (see `_checkConstraint`) to keep what the user signs flat and
/// easy to display. Example:
///
/// Constraint[] memory subs = new Constraint[](2);
/// subs[0] = Constraint({ constraintType: ConstraintType.EQ, referenceData: abi.encode(bytes32(uint256(0))) });
/// subs[1] = Constraint({ constraintType: ConstraintType.GTE, referenceData: abi.encode(bytes32(uint256(100))) });
/// Constraint memory orC = Constraint({ constraintType: ConstraintType.OR, referenceData: abi.encode(subs) });
/// // attach orC to the InputParam; passes iff value == 0 OR value >= 100
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];
bytes32 returnValue;
assembly {
returnValue := mload(add(rawValue, add(0x20, mul(i, 0x20))))
}
if (constraint.constraintType == ConstraintType.EQ) {
require(returnValue == bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.EQ));
} else if (constraint.constraintType == ConstraintType.GTE) {
require(returnValue >= bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.GTE));
} else if (constraint.constraintType == ConstraintType.LTE) {
require(returnValue <= bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.LTE));
} else if (constraint.constraintType == ConstraintType.IN) {
(bytes32 lowerBound, bytes32 upperBound) = abi.decode(constraint.referenceData, (bytes32, bytes32));
require(returnValue >= lowerBound && returnValue <= upperBound, ConstraintNotMet(ConstraintType.IN));
} else {
revert InvalidConstraintType();
uint256 len = constraints.length;
for (uint256 i; i < len;) {
Constraint memory c = constraints[i];
bytes32 value;
assembly {
value := mload(add(rawValue, add(0x20, mul(i, 0x20))))
}
if (c.constraintType == ConstraintType.OR) {
Constraint[] memory subs = abi.decode(c.referenceData, (Constraint[]));
uint256 subsLen = subs.length;
bool anyMet;
for (uint256 j; j < subsLen;) {
if (_checkConstraint(value, subs[j])) {
anyMet = true;
break;
}
unchecked {
++j;
}
}
if (!anyMet) revert ConstraintNotMet(c.constraintType);
} else {
if (!_checkConstraint(value, c)) revert ConstraintNotMet(c.constraintType);
}
unchecked {
++i;
}
}
}

/// @dev Returns true if value satisfies constraint c. OR is rejected here: nested OR is not
/// supported, so only leaf constraints may appear inside an OR's sub-array.
function _checkConstraint(bytes32 value, Constraint memory c) private pure returns (bool) {
ConstraintType ct = c.constraintType;
if (ct == ConstraintType.EQ) {
return value == bytes32(c.referenceData);
} else if (ct == ConstraintType.GTE) {
return value >= bytes32(c.referenceData);
} else if (ct == ConstraintType.LTE) {
return value <= bytes32(c.referenceData);
} else if (ct == ConstraintType.IN) {
(bytes32 lower, bytes32 upper) = abi.decode(c.referenceData, (bytes32, bytes32));
return value >= lower && value <= upper;
} else if (ct == ConstraintType.GTE_SIGNED) {
return int256(uint256(value)) >= int256(uint256(bytes32(c.referenceData)));
} else if (ct == ConstraintType.LTE_SIGNED) {
return int256(uint256(value)) <= int256(uint256(bytes32(c.referenceData)));
} else {
revert InvalidConstraintType();
}
}

Expand Down
10 changes: 5 additions & 5 deletions contracts/ComposableExecutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC
function _executeComposable(
ComposableExecution[] calldata cExecutions,
address account,
function(Execution memory execution) internal returns(bytes[] memory) executeExecutionFunction
function(Execution memory execution) internal returns (bytes[] memory) executeExecutionFunction
)
internal
{
Expand All @@ -107,10 +107,10 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC

/// @dev function to be used as an argument for _executeComposable in case of regular call
function _executeExecutionCall(Execution memory execution) internal returns (bytes[] memory) {
return IERC7579Account(msg.sender).executeFromExecutor({
mode: ModeLib.encodeSimpleSingle(),
executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData)
});
return IERC7579Account(msg.sender)
.executeFromExecutor({
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
Expand Down
15 changes: 7 additions & 8 deletions contracts/types/ComposabilityDataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,29 @@ 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
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
enum ConstraintType {
EQ, // Equal to
GTE, // Greater than or equal to
LTE, // Less than or equal to
IN // In range

EQ, // Equal to (bitwise equality; suitable for signed, unsigned, addresses, bytes32)
GTE, // Greater than or equal to (unsigned)
LTE, // Less than or equal to (unsigned)
IN, // In range [lower, upper] (bytes32 comparison; suitable for unsigned ranges and same-sign signed ranges)
GTE_SIGNED, // Greater than or equal to (signed int256)
LTE_SIGNED, // Less than or equal to (signed int256)
OR // At least one sub-constraint must pass; referenceData = abi.encode(Constraint[]); sub-constraints must be leaf types (no nested OR)
}

// Constraint for parameter validation
Expand Down
10 changes: 2 additions & 8 deletions test/ComposabilityBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,13 @@ contract ComposabilityTestBase is Test {

function _createRawTargetInputParam(address target) internal returns (InputParam memory) {
return InputParam({
paramType: InputParamType.TARGET,
fetcherType: InputParamFetcherType.RAW_BYTES,
paramData: abi.encode(target),
constraints: emptyConstraints
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
paramType: InputParamType.VALUE, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(value), constraints: emptyConstraints
});
}
}
4 changes: 4 additions & 0 deletions test/mock/DummyContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,8 @@ contract DummyContract {
function payableEmit() external payable {
emit Received(msg.value);
}

function getSignedValue() external pure returns (int256) {
return -42;
}
}
24 changes: 7 additions & 17 deletions test/unit/ComposableExecution_Complex.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase {
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)
paramData: abi.encode(
4, address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector), address(storageContract), SLOT_A
)
});

ComposableExecution[] memory executions = new ComposableExecution[](1);
Expand Down Expand Up @@ -258,18 +260,12 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase {

// tokenIn
inputParams[3] = InputParam({
paramType: InputParamType.CALL_DATA,
fetcherType: InputParamFetcherType.RAW_BYTES,
paramData: abi.encode(tokenIn),
constraints: emptyConstraints
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
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(tokenOut), constraints: emptyConstraints
});

// amountIn
Expand All @@ -290,18 +286,12 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase {

// deadline
inputParams[7] = InputParam({
paramType: InputParamType.CALL_DATA,
fetcherType: InputParamFetcherType.RAW_BYTES,
paramData: abi.encode(deadline),
constraints: emptyConstraints
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
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(fee), constraints: emptyConstraints
});

// === end struct ==
Expand Down
Loading
Loading