Summary
In ModuleManager contract, _installFallbackHandler(module, initData) installs fallback handler without checking the calltype value. This fallback handler cannot be called by Nexus.
Vulnerability Details
_installFallbackHandler(module, initData) installs fallback handler with following lines:
function _installFallbackHandler(address handler, bytes calldata params) internal virtual withRegistry(handler, MODULE_TYPE_FALLBACK) {
if (!IFallback(handler).isModuleType(MODULE_TYPE_FALLBACK)) revert MismatchModuleTypeId(MODULE_TYPE_FALLBACK);
bytes4 selector = bytes4(params[0:4]);
CallType calltype = CallType.wrap(bytes1(params[4]));
bytes memory initData = params[5:];
require(!(selector == bytes4(0x6d61fe70) || selector == bytes4(0x8a91b0e3)), FallbackSelectorForbidden());
require(!_isFallbackHandlerInstalled(selector), FallbackAlreadyInstalledForSelector(selector));
_getAccountStorage().fallbacks[selector] = FallbackHandler(handler, calltype);
IFallback(handler).onInstall(initData);
}
"Calltype is bytes1;" is user-defined type. This calltype variable is decoded from params variable. But there are at most 2 type of calltype defined in the Nexus:
CALLTYPE_STATIC
CALLTYPE_SINGLE
So, it can configure calltype wrong.
Impact
ModuleManager's fallback function defined as:
fallback() external payable override(Receiver) receiverFallback {
FallbackHandler storage $fallbackHandler = _getAccountStorage().fallbacks[msg.sig];
address handler = $fallbackHandler.handler;
CallType calltype = $fallbackHandler.calltype;
require(handler != address(0), MissingFallbackHandler(msg.sig));
if (calltype == CALLTYPE_STATIC) {
assembly {
calldatacopy(0, 0, calldatasize())
mstore(calldatasize(), shl(96, caller()))
if iszero(staticcall(gas(), handler, 0, add(calldatasize(), 20), 0, 0)) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
returndatacopy(0, 0, returndatasize())
return(0, returndatasize())
}
}
if (calltype == CALLTYPE_SINGLE) {
assembly {
calldatacopy(0, 0, calldatasize())
mstore(calldatasize(), shl(96, caller()))
if iszero(call(gas(), handler, 0, 0, add(calldatasize(), 20), 0, 0)) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
returndatacopy(0, 0, returndatasize())
return(0, returndatasize())
}
}
}
It's payable and open to wrong calltype vulnerability. It checks the calltype in here with 2 if statement and it will not catch any of those two if statement and transaction will and with no failure.
Scenario:
Bob installed a fallback handler to his Nexus with wrong calltype (example: 0x20)
Later, Alice wants to call that fallback handler with 1 ether for execution
Both if statements won't be catched because of invalid calltype and 1 ether will be on the Bob's Nexus without failure
Tools Used
Manual review
Recommendations
Following check will solve the problem
@@ -280,6 +281,7 @@ abstract contract ModuleManager is Storage, Receiver, EIP712, IModuleManagerEven
CallType calltype = CallType.wrap(bytes1(params[4]));
+ require(calltype == CALLTYPE_SINGLE || calltype == CALLTYPE_STATIC, InvalidCallType());
bytes memory initData = params[5:];