Skip to content

Commit 60dddc6

Browse files
committed
Gas efficiency refactor:
- specified values must now be of equal size, with the startIndex | length encoded in the first 32 bytes - uses direct bytes comparison, rather than keccak hash
1 parent 12c758f commit 60dddc6

2 files changed

Lines changed: 126 additions & 92 deletions

File tree

src/enforcers/AllowedCalldataAnyOfEnforcer.sol

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { ModeCode } from "../utils/Types.sol";
88

99
/**
1010
* @title AllowedCalldataAnyOfEnforcer
11-
* @dev Like `AllowedCalldataEnforcer`, but the delegator supplies several allowed byte sequences (`bytes[]`).
12-
* @dev At `dataStart`, the execution calldata must exactly match **at least one** of those sequences (each candidate is compared
13-
* over its full length, starting at `dataStart`).
11+
* @dev Like `AllowedCalldataEnforcer`, but the delegator supplies several allowed byte sequences of **equal length**.
12+
* @dev At `startIndex`, the execution calldata must exactly match **at least one** of those sequences (each candidate is compared
13+
* over `valueLength` bytes, starting at `startIndex`).
1414
* @dev This enforcer operates only in single execution call type and with default execution mode.
1515
* @dev Prefer static or fixed-layout regions of calldata; validating dynamic types remains possible but is more error-prone,
1616
* same as for `AllowedCalldataEnforcer`.
@@ -21,11 +21,11 @@ contract AllowedCalldataAnyOfEnforcer is CaveatEnforcer {
2121
////////////////////////////// Public Methods //////////////////////////////
2222

2323
/**
24-
* @notice Allows the delegator to restrict calldata so that one of several allowed slices matches at a fixed offset.
25-
* @dev For each candidate `v` in the decoded array, checks `callData[dataStart : dataStart + len(v)] == v`.
26-
* @param _terms Packed header plus ABI-encoded `bytes[]`:
27-
* - the first 32 bytes: `uint256` start index in the execution call data (same layout as `AllowedCalldataEnforcer`)
28-
* - the remainder: `abi.encode(bytes[])` where each element is a non-empty candidate byte string
24+
* @notice Allows the delegator to restrict calldata so that one of several equal-length slices matches at a fixed offset.
25+
* @dev For each candidate, checks `callData[startIndex : startIndex + valueLength] == candidate`.
26+
* @param _terms Binary layout:
27+
* - **First 32 bytes:** `uint128 startIndex` (high 128 bits) | `uint128 valueLength` (low 128 bits) of one big-endian word.
28+
* - **Remainder:** `candidateCount` candidates concatenated, each exactly `valueLength` bytes (so `len(remainder) == candidateCount * valueLength`).
2929
* @param _mode The execution mode. (Must be Single callType, Default execType)
3030
* @param _executionCallData The execution the delegate is trying to execute.
3131
*/
@@ -48,39 +48,53 @@ contract AllowedCalldataAnyOfEnforcer is CaveatEnforcer {
4848
}
4949

5050
/**
51-
* @notice Decodes the terms used in this CaveatEnforcer.
51+
* @notice Decodes and validates the terms used in this CaveatEnforcer.
52+
* @dev After reading `valueLength` from the header word, requires `valueLength >= 1`, a non-empty remainder, and that the
53+
* remainder length is a multiple of `valueLength`.
5254
* @param _terms Encoded data used during the execution hooks.
53-
* @return dataStart_ The start index in the execution's call data.
54-
* @return values_ ABI-decoded array of candidate byte strings; each element must be at least one byte long.
55+
* @return startIndex_ Start index in the execution's call data.
56+
* @return valueLength_ Length of every candidate slice and of the compared execution calldata window.
57+
* @return candidateCount_ Number of candidates in the concatenated tail (`(len(_terms) - 32) / valueLength_`).
5558
*/
56-
function getTermsInfo(bytes calldata _terms) public pure returns (uint256 dataStart_, bytes[] memory values_) {
57-
require(_terms.length >= 32, "AllowedCalldataAnyOfEnforcer:invalid-terms-size");
58-
dataStart_ = uint256(bytes32(_terms[0:32]));
59-
values_ = abi.decode(_terms[32:], (bytes[]));
60-
require(values_.length > 0, "AllowedCalldataAnyOfEnforcer:no-allowed-values");
61-
for (uint256 i = 0; i < values_.length; ++i) {
62-
require(values_[i].length >= 1, "AllowedCalldataAnyOfEnforcer:invalid-value-length");
63-
}
59+
function getTermsInfo(bytes calldata _terms)
60+
public
61+
pure
62+
returns (uint128 startIndex_, uint128 valueLength_, uint256 candidateCount_)
63+
{
64+
require(_terms.length > 32, "AllowedCalldataAnyOfEnforcer:invalid-terms-size");
65+
uint256 metadataWord_ = uint256(bytes32(_terms[0:32]));
66+
startIndex_ = uint128(metadataWord_ >> 128);
67+
valueLength_ = uint128(metadataWord_);
68+
69+
require(valueLength_ >= 1, "AllowedCalldataAnyOfEnforcer:invalid-value-length");
70+
71+
uint256 concatenatedValuesLength_ = _terms.length - 32;
72+
require(concatenatedValuesLength_ != 0, "AllowedCalldataAnyOfEnforcer:no-allowed-values");
73+
require(concatenatedValuesLength_ % uint256(valueLength_) == 0, "AllowedCalldataAnyOfEnforcer:invalid-values-padding");
74+
75+
candidateCount_ = concatenatedValuesLength_ / uint256(valueLength_);
6476
}
6577

6678
/**
67-
* @notice Validates that the execution calldata matches one of the allowed slices at `dataStart`.
79+
* @notice Validates that the execution calldata matches one of the allowed slices at `startIndex`.
6880
* @param _terms Encoded terms (see `beforeHook`).
6981
* @param _executionCallData The encoded single execution payload.
7082
*/
7183
function _validateCalldata(bytes calldata _terms, bytes calldata _executionCallData) private pure {
72-
(uint256 dataStart_, bytes[] memory values_) = getTermsInfo(_terms);
84+
(uint128 startIndex_, uint128 valueLength_, uint256 candidateCount_) = getTermsInfo(_terms);
85+
86+
uint256 dataStart_ = uint256(startIndex_);
87+
uint256 lengthToMatch_ = uint256(valueLength_);
7388
(,, bytes calldata callData_) = _executionCallData.decodeSingle();
7489

90+
require(dataStart_ + lengthToMatch_ <= callData_.length, "AllowedCalldataAnyOfEnforcer:invalid-calldata-length");
91+
92+
bytes calldata callDataToMatch_ = callData_[dataStart_:dataStart_ + lengthToMatch_];
93+
7594
bool matched_;
76-
uint256 n_ = values_.length;
77-
for (uint256 i = 0; i < n_; ++i) {
78-
bytes memory candidate_ = values_[i];
79-
uint256 len_ = candidate_.length;
80-
if (dataStart_ + len_ > callData_.length) {
81-
continue;
82-
}
83-
if (keccak256(callData_[dataStart_:dataStart_ + len_]) == keccak256(candidate_)) {
95+
for (uint256 i = 0; i < candidateCount_; ++i) {
96+
uint256 offset_ = 32 + i * lengthToMatch_;
97+
if (callDataToMatch_ == _terms[offset_:offset_ + lengthToMatch_]) {
8498
matched_ = true;
8599
break;
86100
}

0 commit comments

Comments
 (0)