Skip to content

Encode the success boolean (alongised the returndata) for calls made in the EXEC_TYPE_TRY mode#6420

Open
Amxx wants to merge 1 commit intoOpenZeppelin:masterfrom
Amxx:fix/L-38-v2
Open

Encode the success boolean (alongised the returndata) for calls made in the EXEC_TYPE_TRY mode#6420
Amxx wants to merge 1 commit intoOpenZeppelin:masterfrom
Amxx:fix/L-38-v2

Conversation

@Amxx
Copy link
Collaborator

@Amxx Amxx commented Mar 19, 2026

Fixes L-38

Alternative to #6419

ERC-7579 execution results are surfaced to executor modules through executeFromExecutor, which returns bytes[] memory returnData. AccountERC7579 forwards execution to ERC7579Utils through its internal _execute, which performs the low-level call in _call and validates the mode in _validateExecutionMode.

In TRY mode, _validateExecutionMode emits ERC7579TryExecuteFail when success == false but always returns the raw returndata (line 252). Since contracts cannot read events during execution, on-chain executor modules cannot reliably distinguish successful return values from revert payloads. Revert data is controlled by the callee and can be crafted to decode into plausible values under an expected return schema, creating logic confusion when modules branch on TRY-mode returnData.

Consider returning an explicit per-call success signal (for example, a parallel bool[], or encoding each element as abi.encode(success, returndata)), or clearly documenting that TRY-mode returnData must not be used for on-chain branching and is only suitable for off-chain inspection.

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@Amxx Amxx requested a review from a team as a code owner March 19, 2026 14:04
@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

⚠️ No Changeset found

Latest commit: 2d0d17b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Walkthrough

This pull request modifies the _validateExecutionMode function in the ERC7579 utilities to return explicitly in each execution path rather than falling through to a final return statement. The EXECTYPE_DEFAULT path now returns the result of Address.verifyCallResult() directly, while EXECTYPE_TRY returns an ABI-encoded pair of success and return data. Corresponding test updates add a helper function for encoding error strings and extend assertions to validate both ERC7579TryExecuteFail events and subsequent return data events, including per-item flags for batch executions.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: encoding the success boolean alongside returndata for EXEC_TYPE_TRY mode calls.
Description check ✅ Passed The description is related to the changeset, explaining the issue being fixed and the rationale for encoding success signals in TRY mode.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/account/utils/draft-ERC7579Utils.test.js (1)

91-104: Consider adding test coverage for successful EXEC_TYPE_TRY executions.

The tests only verify the return format for TRY mode when calls fail. It would be valuable to add tests confirming that successful calls in TRY mode also return abi.encode(true, returndata) with the correct success boolean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/utils/draft-ERC7579Utils.test.js` around lines 91 - 104, Add a
test that mirrors the failing TRY-case but for a successful call: call
utils.$execSingle with EXEC_TYPE_TRY using encodeSingle(this.target, value,
this.target.interface.encodeFunctionData('mockFunction')) (or another
non-reverting function on CallReceiverMock), then assert it does NOT emit
'ERC7579TryExecuteFail' and DOES emit 'return$execSingle'
withArgs([coder.encode(['bool','bytes'], [true, returndata])]) where returndata
is the raw bytes returned by the mock function (obtain via
this.target.interface.encodeFunctionResult or by calling the function locally to
capture its return data); use the same EXEC_TYPE_TRY, $execSingle, and
return$execSingle symbols as in the existing test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/account/utils/draft-ERC7579Utils.test.js`:
- Around line 248-252: The test uses the semantic constant CALL_TYPE_CALL where
the delegate-call result should assert on the numeric index 0; update the
assertion to use the explicit index 0 instead of CALL_TYPE_CALL in the .withArgs
for ERC7579TryExecuteFail and/or return$execDelegateCall so the expected event
payload matches the delegate-call-at-index-0 shape (keep other values:
encodeErrorString('CallReceiverMock: reverting') and
coder.encode(['bool','bytes'], [false, encodeErrorString('CallReceiverMock:
reverting')]) unchanged). Locate the failing assertion around
this.utils.$execDelegateCall(...) that references CALL_TYPE_CALL and replace it
with 0.
- Around line 99-103: The test is using CALL_TYPE_CALL as the first arg to
ERC7579TryExecuteFail (which is actually the uint256 batchExecutionIndex)
causing a semantic mismatch; update the assertion in
test/account/utils/draft-ERC7579Utils.test.js that calls this.utils.$execSingle
so the ERC7579TryExecuteFail expectation uses the explicit numeric index (e.g.,
0) instead of CALL_TYPE_CALL, i.e. change withArgs(CALL_TYPE_CALL, ...) to
withArgs(0, ...), leaving the rest of the expectation (encodeErrorString payload
and the return$execSingle assertion) unchanged.
- Around line 183-193: The test asserts the wrong event argument by using
CALL_TYPE_BATCH (which equals '0x01') instead of the explicit numeric index for
the failing item; update the .withArgs for the ERC7579TryExecuteFail event to
use the explicit index 1 (the second batch item) rather than CALL_TYPE_BATCH so
the expected event matches the actual failing call in this.utils.$execBatch
invoked with EXEC_TYPE_TRY; keep the rest of the .withArgs checks for
return$execBatch as-is and ensure you change only the event index reference
(ERC7579TryExecuteFail) to the literal 1.

---

Nitpick comments:
In `@test/account/utils/draft-ERC7579Utils.test.js`:
- Around line 91-104: Add a test that mirrors the failing TRY-case but for a
successful call: call utils.$execSingle with EXEC_TYPE_TRY using
encodeSingle(this.target, value,
this.target.interface.encodeFunctionData('mockFunction')) (or another
non-reverting function on CallReceiverMock), then assert it does NOT emit
'ERC7579TryExecuteFail' and DOES emit 'return$execSingle'
withArgs([coder.encode(['bool','bytes'], [true, returndata])]) where returndata
is the raw bytes returned by the mock function (obtain via
this.target.interface.encodeFunctionResult or by calling the function locally to
capture its return data); use the same EXEC_TYPE_TRY, $execSingle, and
return$execSingle symbols as in the existing test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f0fb6692-2336-48e7-be80-12a70814d2fb

📥 Commits

Reviewing files that changed from the base of the PR and between 45f032d and 2d0d17b.

📒 Files selected for processing (2)
  • contracts/account/utils/draft-ERC7579Utils.sol
  • test/account/utils/draft-ERC7579Utils.test.js

Comment on lines 99 to +103
await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
),
);
.withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execSingle')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use explicit numeric index instead of CALL_TYPE_CALL for batchExecutionIndex.

The ERC7579TryExecuteFail event's first parameter is uint256 batchExecutionIndex, but the test uses CALL_TYPE_CALL (a call type constant). This works only because CALL_TYPE_CALL = '0x00' happens to equal 0, which is the correct index for a single execution.

Using call type constants as indices is semantically misleading and could cause confusion or bugs during refactoring.

Suggested fix
      await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
        .to.emit(this.utils, 'ERC7579TryExecuteFail')
-        .withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
+        .withArgs(0, encodeErrorString('CallReceiverMock: reverting'))
        .to.emit(this.utils, 'return$execSingle')
        .withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
),
);
.withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execSingle')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(0, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execSingle')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/utils/draft-ERC7579Utils.test.js` around lines 99 - 103, The
test is using CALL_TYPE_CALL as the first arg to ERC7579TryExecuteFail (which is
actually the uint256 batchExecutionIndex) causing a semantic mismatch; update
the assertion in test/account/utils/draft-ERC7579Utils.test.js that calls
this.utils.$execSingle so the ERC7579TryExecuteFail expectation uses the
explicit numeric index (e.g., 0) instead of CALL_TYPE_CALL, i.e. change
withArgs(CALL_TYPE_CALL, ...) to withArgs(0, ...), leaving the rest of the
expectation (encodeErrorString payload and the return$execSingle assertion)
unchanged.

Comment on lines 183 to +193
await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_BATCH,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
.withArgs(CALL_TYPE_BATCH, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execBatch')
.withArgs([
coder.encode(
['bool', 'bytes'],
[true, this.target.interface.encodeFunctionResult('mockFunction', ['0x1234'])],
),
);
coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')]),
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue: use explicit index 1 instead of CALL_TYPE_BATCH.

The failing call is the second item (index 1), and CALL_TYPE_BATCH = '0x01' coincidentally equals 1. The return value assertions correctly validate the new abi.encode(success, returndata) format for each batch item.

Suggested fix
      await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
        .to.emit(this.utils, 'ERC7579TryExecuteFail')
-        .withArgs(CALL_TYPE_BATCH, encodeErrorString('CallReceiverMock: reverting'))
+        .withArgs(1, encodeErrorString('CallReceiverMock: reverting'))
        .to.emit(this.utils, 'return$execBatch')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_BATCH,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
.withArgs(CALL_TYPE_BATCH, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execBatch')
.withArgs([
coder.encode(
['bool', 'bytes'],
[true, this.target.interface.encodeFunctionResult('mockFunction', ['0x1234'])],
),
);
coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')]),
]);
await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(1, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execBatch')
.withArgs([
coder.encode(
['bool', 'bytes'],
[true, this.target.interface.encodeFunctionResult('mockFunction', ['0x1234'])],
),
coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')]),
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/utils/draft-ERC7579Utils.test.js` around lines 183 - 193, The
test asserts the wrong event argument by using CALL_TYPE_BATCH (which equals
'0x01') instead of the explicit numeric index for the failing item; update the
.withArgs for the ERC7579TryExecuteFail event to use the explicit index 1 (the
second batch item) rather than CALL_TYPE_BATCH so the expected event matches the
actual failing call in this.utils.$execBatch invoked with EXEC_TYPE_TRY; keep
the rest of the .withArgs checks for return$execBatch as-is and ensure you
change only the event index reference (ERC7579TryExecuteFail) to the literal 1.

Comment on lines 248 to +252
await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
),
);
.withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execDelegateCall')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue: use explicit index 0 instead of CALL_TYPE_CALL.

For delegate call at index 0, the numeric value is correct but the constant is semantically incorrect.

Suggested fix
      await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
        .to.emit(this.utils, 'ERC7579TryExecuteFail')
-        .withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
+        .withArgs(0, encodeErrorString('CallReceiverMock: reverting'))
        .to.emit(this.utils, 'return$execDelegateCall')
        .withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
ethers.solidityPacked(
['bytes4', 'bytes'],
[selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])],
),
);
.withArgs(CALL_TYPE_CALL, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execDelegateCall')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(0, encodeErrorString('CallReceiverMock: reverting'))
.to.emit(this.utils, 'return$execDelegateCall')
.withArgs([coder.encode(['bool', 'bytes'], [false, encodeErrorString('CallReceiverMock: reverting')])]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/utils/draft-ERC7579Utils.test.js` around lines 248 - 252, The
test uses the semantic constant CALL_TYPE_CALL where the delegate-call result
should assert on the numeric index 0; update the assertion to use the explicit
index 0 instead of CALL_TYPE_CALL in the .withArgs for ERC7579TryExecuteFail
and/or return$execDelegateCall so the expected event payload matches the
delegate-call-at-index-0 shape (keep other values:
encodeErrorString('CallReceiverMock: reverting') and
coder.encode(['bool','bytes'], [false, encodeErrorString('CallReceiverMock:
reverting')]) unchanged). Locate the failing assertion around
this.utils.$execDelegateCall(...) that references CALL_TYPE_CALL and replace it
with 0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant