HardhatFoundry
30,000 USDC
View results
Submission Details
Severity: high
Invalid

Handling execution may revert despite being in `TRY` mode

Summary

Handling execution may revert despite being in TRY mode (return on failure).

Vulnerability Details

Nexus::execute(...) is designed to execute transactions in various modes:

function execute(ExecutionMode mode, bytes calldata executionCalldata) external payable onlyEntryPointOrSelf withHook {
(CallType callType, ExecType execType) = mode.decodeBasic();
if (callType == CALLTYPE_SINGLE) {
_handleSingleExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_BATCH) {
_handleBatchExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_DELEGATECALL) {
_handleDelegateCallExecution(executionCalldata, execType);
} else {
revert UnsupportedCallType(callType);
}
}

What if the call type is CALLTYPE_SINGLE? well, ExecutionHelper::_handleSingleExecution(...) is triggered:

...
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleSingleExecution(bytes calldata executionCalldata, ExecType execType) internal {
(address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle();
if (execType == EXECTYPE_DEFAULT) _execute(target, value, callData);
else if (execType == EXECTYPE_TRY) _tryExecute(target, value, callData);
else revert UnsupportedExecType(execType);
}

To generalize, if callType == CALLTYPE_X, ExecutionHelper::_handleXExecution(...) is triggered. It either calls ExecutionHelper::_execute(...) fore revert on failure, or ExecutionHelper::_tryExecute(...) for return on failure.

Now, withHook modifier enforces hooks. A hook does pre and post checks on an execution logic:

modifier withHook() {
address hook = _getHook();
if (hook == address(0)) {
_;
} else {
bytes memory hookData = IHook(hook).preCheck(msg.sender, msg.value, msg.data);
_;
IHook(hook).postCheck(hookData);
}
}

If one of the checks reverts for any reason,withHook reverts the entire execution, even when operating in TRY mode (which is intended to return on failure rather than revert).

Proof of Concept

  • Modify MockHook::postCheck(...) to include a revert:

...
contract MockHook is IModule {
...
function postCheck(bytes calldata hookData) external {
hookData;
emit PostCheckCalled();
+ revert("REVERT_FROM_HOOK");
}
...
}
  • Execute the following test using forge test --mt test_poc:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "../shared/TestModuleManagement_Base.t.sol";
import "../shared/TestAccountExecution_Base.t.sol";
/// @notice Tests for reverting despite being in `TRY` mode.
contract POC is TestModuleManagement_Base, TestAccountExecution_Base {
function setUp() public {
setUpModuleManagement_Base();
setUpTestAccountExecution_Base();
}
function test_poc() public {
// Prepare call data for installing the hook module
bytes memory callData = abi.encodeWithSelector(IModuleManager.installModule.selector, MODULE_TYPE_HOOK, address(mockHook), "");
// Install the hook module
installModule(callData, MODULE_TYPE_HOOK, address(mockHook), EXECTYPE_DEFAULT);
// Assert that the hook module is now installed
assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_HOOK, address(mockHook), ""), "Hook module should be installed");
Execution[] memory execution = new Execution[](1);
execution[0] = Execution(address(counter), 0, abi.encodeWithSelector(Counter.incrementNumber.selector));
ExecType catchExecType = ExecType.wrap(bytes1(0x01));
CallType callType = CALLTYPE_SINGLE;
ExecutionMode mode = ModeLib.encodeCustom(callType, catchExecType);
// Execute in `TRY` mode
vm.expectRevert("REVERT_FROM_HOOK");
prank(address(BOB_ACCOUNT));
BOB_ACCOUNT.execute(mode, abi.encode(execution));
}
}
➜ 2024-07-biconomy git:(main) ✗ forge test --mt test_poc
[PASS] test_poc() (gas: 190406)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.39ms (1.05ms CPU time)
Ran 1 test suite in 122.82ms (15.39ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Tools Used

  • Manual review.

  • Foundry.

Recommendation

Restrict the hook execution to revert mode only. Update Nexus::execute(...):

+ function execute(...) external payable onlyEntryPointOrSelf {
- function execute(...) external payable onlyEntryPointOrSelf withHook {

Then, restrict withHook modifier to ExecutionHelper::_execute(...) only:

+ function _execute(...) internal virtual returns (bytes memory result) withHook {
- function _execute(...) internal virtual returns (bytes memory result) {
Updates

Lead Judging Commences

0xnevi Lead Judge
about 1 year ago
0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Appeal created

x18a6 Submitter
about 1 year ago
0xnevi Lead Judge
about 1 year ago
x18a6 Submitter
about 1 year ago
0xnevi Lead Judge
about 1 year ago
0xgenaudits Judge
about 1 year ago
x18a6 Submitter
about 1 year ago
x18a6 Submitter
about 1 year ago
0xnevi Lead Judge
about 1 year ago
0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.