Skip to content

Commit 92bade9

Browse files
committed
feat: add signed integer constraints and OR predicate composition
Community feedback on ERC-8211 identified two gaps in the constraint system: - Unsigned-only GTE/LTE constraints are incorrect for int256 values (two's complement makes negative numbers appear greater than positives in raw bytes32 comparison). - No way to express OR logic without deploying helper contracts.
1 parent e22f819 commit 92bade9

8 files changed

Lines changed: 598 additions & 95 deletions

contracts/ComposableExecutionLib.sol

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,7 @@ library ComposableExecutionLib {
6262
// we don't restrict it since some calls may want to call address(0)
6363
// if a param with VALUE type was not provided, it will be 0
6464
// this is even more often case, as many calls happen with 0 value
65-
return Execution({
66-
target: composedTarget,
67-
value: composedValue,
68-
callData: composedCalldata
69-
});
65+
return Execution({ target: composedTarget, value: composedValue, callData: composedCalldata });
7066
}
7167

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

10096
// expect paramData to be abi.encodePacked(address token, address account)
10197
// Validate exact length requirement
102-
require(paramData.length == 40,
103-
InvalidParameterEncoding("Invalid paramData length"));
98+
require(paramData.length == 40, InvalidParameterEncoding("Invalid paramData length"));
10499
assembly {
105100
tokenAddr := shr(96, calldataload(paramData.offset))
106101
account := shr(96, calldataload(add(paramData.offset, 0x14)))
@@ -170,31 +165,54 @@ library ComposableExecutionLib {
170165
}
171166
}
172167

173-
/// @dev Validate the constraints => compare the value with the reference data
168+
/// @dev Validate the constraints => compare the value with the reference data.
169+
/// Each constraints[i] is checked against the i-th 32-byte word of rawValue (AND semantics).
170+
/// Use ConstraintType.OR to express OR semantics within a single word position.
174171
function _validateConstraints(bytes memory rawValue, Constraint[] calldata constraints) private pure {
175-
if (constraints.length > 0) {
176-
for (uint256 i; i < constraints.length; i++) {
177-
Constraint memory constraint = constraints[i];
178-
bytes32 returnValue;
179-
assembly {
180-
returnValue := mload(add(rawValue, add(0x20, mul(i, 0x20))))
181-
}
182-
if (constraint.constraintType == ConstraintType.EQ) {
183-
require(returnValue == bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.EQ));
184-
} else if (constraint.constraintType == ConstraintType.GTE) {
185-
require(returnValue >= bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.GTE));
186-
} else if (constraint.constraintType == ConstraintType.LTE) {
187-
require(returnValue <= bytes32(constraint.referenceData), ConstraintNotMet(ConstraintType.LTE));
188-
} else if (constraint.constraintType == ConstraintType.IN) {
189-
(bytes32 lowerBound, bytes32 upperBound) = abi.decode(constraint.referenceData, (bytes32, bytes32));
190-
require(returnValue >= lowerBound && returnValue <= upperBound, ConstraintNotMet(ConstraintType.IN));
191-
} else {
192-
revert InvalidConstraintType();
172+
uint256 len = constraints.length;
173+
for (uint256 i; i < len; i++) {
174+
Constraint memory c = constraints[i];
175+
bytes32 value;
176+
assembly {
177+
value := mload(add(rawValue, add(0x20, mul(i, 0x20))))
178+
}
179+
if (c.constraintType == ConstraintType.OR) {
180+
Constraint[] memory subs = abi.decode(c.referenceData, (Constraint[]));
181+
bool anyMet;
182+
for (uint256 j; j < subs.length; j++) {
183+
if (_checkConstraint(value, subs[j])) {
184+
anyMet = true;
185+
break;
186+
}
193187
}
188+
if (!anyMet) revert ConstraintNotMet(ConstraintType.OR);
189+
} else {
190+
if (!_checkConstraint(value, c)) revert ConstraintNotMet(c.constraintType);
194191
}
195192
}
196193
}
197194

195+
/// @dev Returns true if value satisfies constraint c. OR nesting is not supported.
196+
function _checkConstraint(bytes32 value, Constraint memory c) private pure returns (bool) {
197+
ConstraintType ct = c.constraintType;
198+
if (ct == ConstraintType.EQ) {
199+
return value == bytes32(c.referenceData);
200+
} else if (ct == ConstraintType.GTE) {
201+
return value >= bytes32(c.referenceData);
202+
} else if (ct == ConstraintType.LTE) {
203+
return value <= bytes32(c.referenceData);
204+
} else if (ct == ConstraintType.IN) {
205+
(bytes32 lower, bytes32 upper) = abi.decode(c.referenceData, (bytes32, bytes32));
206+
return value >= lower && value <= upper;
207+
} else if (ct == ConstraintType.GTE_SIGNED) {
208+
return int256(uint256(value)) >= int256(uint256(bytes32(c.referenceData)));
209+
} else if (ct == ConstraintType.LTE_SIGNED) {
210+
return int256(uint256(value)) <= int256(uint256(bytes32(c.referenceData)));
211+
} else {
212+
revert InvalidConstraintType();
213+
}
214+
}
215+
198216
/// @dev Parse the return data and write to the appropriate storage contract
199217
function _parseReturnDataAndWriteToStorage(
200218
uint256 returnValues,

contracts/ComposableExecutionModule.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC
8484
function _executeComposable(
8585
ComposableExecution[] calldata cExecutions,
8686
address account,
87-
function(Execution memory execution) internal returns(bytes[] memory) executeExecutionFunction
87+
function(Execution memory execution) internal returns (bytes[] memory) executeExecutionFunction
8888
)
8989
internal
9090
{
@@ -107,10 +107,10 @@ contract ComposableExecutionModule is IComposableExecutionModule, IExecutor, ERC
107107

108108
/// @dev function to be used as an argument for _executeComposable in case of regular call
109109
function _executeExecutionCall(Execution memory execution) internal returns (bytes[] memory) {
110-
return IERC7579Account(msg.sender).executeFromExecutor({
111-
mode: ModeLib.encodeSimpleSingle(),
112-
executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData)
113-
});
110+
return IERC7579Account(msg.sender)
111+
.executeFromExecutor({
112+
mode: ModeLib.encodeSimpleSingle(), executionCalldata: ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData)
113+
});
114114
}
115115

116116
/// @dev function to be used as an argument for _executeComposable in case of delegatecall

contracts/types/ComposabilityDataTypes.sol

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,29 @@ enum InputParamType {
66
TARGET, // The target address
77
VALUE, // The value
88
CALL_DATA // The call data
9-
109
}
1110

1211
// Parameter type for composition
1312
enum InputParamFetcherType {
1413
RAW_BYTES, // Already encoded bytes
1514
STATIC_CALL, // Perform a static call
1615
BALANCE // Get the balance of an address
17-
1816
}
1917

2018
enum OutputParamFetcherType {
2119
EXEC_RESULT, // The return of the execution call
2220
STATIC_CALL // Call to some other function
23-
2421
}
2522

2623
// Constraint type for parameter validation
2724
enum ConstraintType {
28-
EQ, // Equal to
29-
GTE, // Greater than or equal to
30-
LTE, // Less than or equal to
31-
IN // In range
32-
25+
EQ, // Equal to (unsigned / bitwise)
26+
GTE, // Greater than or equal to (unsigned)
27+
LTE, // Less than or equal to (unsigned)
28+
IN, // In range (unsigned)
29+
GTE_SIGNED, // Greater than or equal to (signed int256)
30+
LTE_SIGNED, // Less than or equal to (signed int256)
31+
OR // At least one sub-constraint must pass; referenceData = abi.encode(Constraint[])
3332
}
3433

3534
// Constraint for parameter validation

test/ComposabilityBase.t.sol

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,13 @@ contract ComposabilityTestBase is Test {
6363

6464
function _createRawTargetInputParam(address target) internal returns (InputParam memory) {
6565
return InputParam({
66-
paramType: InputParamType.TARGET,
67-
fetcherType: InputParamFetcherType.RAW_BYTES,
68-
paramData: abi.encode(target),
69-
constraints: emptyConstraints
66+
paramType: InputParamType.TARGET, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(target), constraints: emptyConstraints
7067
});
7168
}
7269

7370
function _createRawValueInputParam(uint256 value) internal returns (InputParam memory) {
7471
return InputParam({
75-
paramType: InputParamType.VALUE,
76-
fetcherType: InputParamFetcherType.RAW_BYTES,
77-
paramData: abi.encode(value),
78-
constraints: emptyConstraints
72+
paramType: InputParamType.VALUE, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(value), constraints: emptyConstraints
7973
});
8074
}
8175
}

test/mock/DummyContract.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,8 @@ contract DummyContract {
110110
function payableEmit() external payable {
111111
emit Received(msg.value);
112112
}
113+
114+
function getSignedValue() external pure returns (int256) {
115+
return -42;
116+
}
113117
}

test/unit/ComposableExecution_Complex.sol

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase {
9494
OutputParam[] memory outputParams = new OutputParam[](1);
9595
outputParams[0] = OutputParam({
9696
fetcherType: OutputParamFetcherType.STATIC_CALL,
97-
paramData: abi.encode(4, address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector), address(storageContract), SLOT_A)
97+
paramData: abi.encode(
98+
4, address(dummyContract), abi.encodeWithSelector(DummyContract.returnMultipleValues.selector), address(storageContract), SLOT_A
99+
)
98100
});
99101

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

259261
// tokenIn
260262
inputParams[3] = InputParam({
261-
paramType: InputParamType.CALL_DATA,
262-
fetcherType: InputParamFetcherType.RAW_BYTES,
263-
paramData: abi.encode(tokenIn),
264-
constraints: emptyConstraints
263+
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(tokenIn), constraints: emptyConstraints
265264
});
266265

267266
// tokenOut
268267
inputParams[4] = InputParam({
269-
paramType: InputParamType.CALL_DATA,
270-
fetcherType: InputParamFetcherType.RAW_BYTES,
271-
paramData: abi.encode(tokenOut),
272-
constraints: emptyConstraints
268+
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(tokenOut), constraints: emptyConstraints
273269
});
274270

275271
// amountIn
@@ -290,18 +286,12 @@ contract ComposableExecutionTestComplexCases is ComposabilityTestBase {
290286

291287
// deadline
292288
inputParams[7] = InputParam({
293-
paramType: InputParamType.CALL_DATA,
294-
fetcherType: InputParamFetcherType.RAW_BYTES,
295-
paramData: abi.encode(deadline),
296-
constraints: emptyConstraints
289+
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(deadline), constraints: emptyConstraints
297290
});
298291

299292
// fee
300293
inputParams[8] = InputParam({
301-
paramType: InputParamType.CALL_DATA,
302-
fetcherType: InputParamFetcherType.RAW_BYTES,
303-
paramData: abi.encode(fee),
304-
constraints: emptyConstraints
294+
paramType: InputParamType.CALL_DATA, fetcherType: InputParamFetcherType.RAW_BYTES, paramData: abi.encode(fee), constraints: emptyConstraints
305295
});
306296

307297
// === end struct ==

0 commit comments

Comments
 (0)