Skip to content

Commit 6f7eef7

Browse files
authored
Post-audit fixes (#48)
* H1: fix validAfter/validUntil * H3: include preVerificationGas * H2: fix unlimited type time range * M3: strip validator header from signature * L11: add ERC165 to EOAKeyValidator * L8: make time ranges non-overlapping * L1: prevent short calldata on fallbacks * L4: domain length restriction fix * L7: ban calls to registry from session * L9: include storage slot gap in session key validator * L3: prevent returnbombing from malicious modules * L5: save only recovery data hash to storage * M2: add address ownership check to session creation to prevent DoS * M4+L2: refine registry adapter behavior * R1: check calltype early for fallbacks * R9: change _addOwner to be internal * R11: only emit event when state changed * R12: tighten session calldata validation * R14: revert on empty time range * R6: remove redundant checks * formatting and linting * M5: clarification * H4: hardcode offset pointer value * M1: add msg.sender to the address proof signed data
1 parent 403bc20 commit 6f7eef7

14 files changed

+253
-172
lines changed

src/ModularSmartAccount.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,11 @@ contract ModularSmartAccount is IMSA, ExecutionHelper, ERC1271Handler, Initializ
124124
if (!_isValidatorInstalled(validator)) {
125125
return ERC7579.VALIDATION_FAILED;
126126
} else {
127+
// Remove the validator header from the signature, to be compatible with 3rd-party validators
128+
PackedUserOperation memory userOpStripped = userOp;
129+
userOpStripped.signature = userOp.signature[20:];
127130
// bubble up the return value of the validator module
128-
validSignature = ERC7579.IValidator(validator).validateUserOp(userOp, userOpHash);
131+
validSignature = ERC7579.IValidator(validator).validateUserOp(userOpStripped, userOpHash);
129132
}
130133
}
131134

src/core/ModuleManager.sol

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.21;
33

44
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
5+
import { LibERC7579 } from "solady/accounts/LibERC7579.sol";
56

67
import { RegistryAdapter } from "./RegistryAdapter.sol";
78
import "../interfaces/IERC7579Module.sol" as ERC7579;
@@ -16,8 +17,10 @@ abstract contract ModuleManager is RegistryAdapter {
1617
using EnumerableSet for EnumerableSet.AddressSet;
1718

1819
error InvalidModule(address module);
20+
error NotEnoughData();
1921
error NoFallbackHandler(bytes4 selector);
2022
error CannotRemoveLastValidator();
23+
error InvalidCallType(bytes1 calltype);
2124
error SelectorAlreadyUsed(bytes4 selector);
2225
error AlreadyInstalled(address module);
2326
error NotInstalled(address module);
@@ -61,19 +64,34 @@ abstract contract ModuleManager is RegistryAdapter {
6164
_;
6265
}
6366

64-
/// @dev Handles uninstall failures by bubbling up or emitting `ModuleUnlinked` when forced.
65-
/// @param reason Revert data returned by the module.
66-
/// @param force Whether the uninstall should proceed even if the module reverts.
67-
/// @param moduleTypeId Identifier of the module type.
68-
/// @param module Address of the module being removed.
69-
function _onUninstallFail(bytes memory reason, bool force, uint256 moduleTypeId, address module) internal {
70-
if (!force) {
67+
function _uninstallModule(bytes memory deinitData, address module, uint256 moduleTypeId, bool force) internal {
68+
bytes memory callData = abi.encodeCall(ERC7579.IModule.onUninstall, deinitData);
69+
uint256 gasLimit = force ? gasleft() / 2 : gasleft();
70+
bool success;
71+
assembly {
72+
success := call(gasLimit, module, 0, add(callData, 0x20), mload(callData), 0, 0)
73+
}
74+
if (success) {
75+
return;
76+
}
77+
if (force) {
78+
uint256 copySize;
7179
assembly {
72-
// forward revert data
73-
revert(add(reason, 32), mload(reason))
80+
copySize := returndatasize()
7481
}
82+
// cap return data size at 256 bytes
83+
copySize = copySize > 256 ? 256 : copySize;
84+
bytes memory returnData = new bytes(copySize);
85+
assembly {
86+
returndatacopy(add(returnData, 0x20), 0, copySize)
87+
}
88+
emit ModuleUnlinked(moduleTypeId, module, returnData);
7589
} else {
76-
emit ModuleUnlinked(moduleTypeId, module, reason);
90+
assembly {
91+
let size := returndatasize()
92+
returndatacopy(0, 0, size)
93+
revert(0, size)
94+
}
7795
}
7896
}
7997

@@ -96,10 +114,7 @@ abstract contract ModuleManager is RegistryAdapter {
96114
function _uninstallValidator(address validator, bytes calldata data, bool force) internal {
97115
require($moduleManager().$validators.remove(validator), NotInstalled(validator));
98116
require($moduleManager().$validators.length() > 0, CannotRemoveLastValidator());
99-
try ERC7579.IValidator(validator).onUninstall(data) { }
100-
catch (bytes memory reason) {
101-
_onUninstallFail(reason, force, ERC7579.MODULE_TYPE_VALIDATOR, validator);
102-
}
117+
_uninstallModule(data, validator, ERC7579.MODULE_TYPE_VALIDATOR, force);
103118
}
104119

105120
/// @dev Checks whether a validator module is currently installed.
@@ -127,10 +142,7 @@ abstract contract ModuleManager is RegistryAdapter {
127142
/// @param force Whether failures should be swallowed and logged instead of bubbled.
128143
function _uninstallExecutor(address executor, bytes calldata data, bool force) internal {
129144
require($moduleManager().$executors.remove(executor), NotInstalled(executor));
130-
try ERC7579.IExecutor(executor).onUninstall(data) { }
131-
catch (bytes memory reason) {
132-
_onUninstallFail(reason, force, ERC7579.MODULE_TYPE_EXECUTOR, executor);
133-
}
145+
_uninstallModule(data, executor, ERC7579.MODULE_TYPE_EXECUTOR, force);
134146
}
135147

136148
/// @dev Checks whether an executor module is currently installed.
@@ -150,6 +162,10 @@ abstract contract ModuleManager is RegistryAdapter {
150162
function _installFallbackHandler(address handler, bytes calldata params) internal virtual {
151163
bytes4 selector = bytes4(params[0:4]);
152164
bytes1 calltype = params[4];
165+
require(
166+
calltype == LibERC7579.CALLTYPE_SINGLE || calltype == LibERC7579.CALLTYPE_STATICCALL,
167+
InvalidCallType(calltype)
168+
);
153169
bytes calldata initData = params[5:];
154170
require(!_isFallbackHandlerInstalled(selector), SelectorAlreadyUsed(selector));
155171
$moduleManager().$fallbacks[selector] = FallbackHandler(handler, calltype);
@@ -167,10 +183,7 @@ abstract contract ModuleManager is RegistryAdapter {
167183
FallbackHandler memory activeFallback = $moduleManager().$fallbacks[selector];
168184
require(activeFallback.handler == handler, NotInstalled(handler));
169185
$moduleManager().$fallbacks[selector] = FallbackHandler(address(0), 0);
170-
try ERC7579.IFallback(handler).onUninstall(_deInitData) { }
171-
catch (bytes memory reason) {
172-
_onUninstallFail(reason, force, ERC7579.MODULE_TYPE_FALLBACK, handler);
173-
}
186+
_uninstallModule(_deInitData, handler, ERC7579.MODULE_TYPE_FALLBACK, force);
174187
}
175188

176189
/// @dev Checks whether any fallback handler is set for a selector.
@@ -202,6 +215,10 @@ abstract contract ModuleManager is RegistryAdapter {
202215

203216
/// @dev Delegates calls to the registered fallback handler or handles ERC token callbacks.
204217
fallback() external payable {
218+
if (msg.data.length > 0 && msg.data.length < 4) {
219+
revert NotEnoughData();
220+
}
221+
205222
FallbackHandler storage $fallbackHandler = $moduleManager().$fallbacks[msg.sig];
206223
address handler = $fallbackHandler.handler;
207224
bytes1 calltype = $fallbackHandler.calltype;

src/core/RegistryAdapter.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,19 @@ abstract contract RegistryAdapter is AccountBase {
4040
external
4141
onlyEntryPointOrSelf
4242
{
43+
IERC7484Registry oldRegistry = $registry;
4344
$registry = registry;
44-
if (attesters.length > 0) {
45+
if (address(oldRegistry) != address(0)) {
46+
// Clean data from old registry
47+
oldRegistry.trustAttesters(0, new address[](0));
48+
}
49+
if (address(registry) != address(0)) {
4550
registry.trustAttesters(threshold, attesters);
4651
}
4752
emit ERC7484RegistryConfigured(address(registry));
4853
}
54+
55+
function getRegistry() external view returns (IERC7484Registry) {
56+
return $registry;
57+
}
4958
}

src/interfaces/IMSA.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ interface IMSA is IERC7579Account, IERC4337Account {
1010
// Error thrown when account installs/unistalls module with mismatched input `moduleTypeId`
1111
error MismatchModuleTypeId(uint256 moduleTypeId);
1212

13-
/// @dev Initializes the account. Function might be called directly, or by a Factory
13+
/// @notice Initializes the account. Function might be called directly, or by a Factory.
14+
/// @dev All passed in modules have to be unique, and of exactly one module type.
1415
/// @param modules Array of module addresses to be installed in the account
1516
/// @param data Array of initialization data corresponding to each module
1617
function initializeAccount(address[] calldata modules, bytes[] calldata data) external payable;

src/libraries/SessionLib.sol

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ library SessionLib {
2121
error ZeroSigner();
2222
error InvalidSigner(address recovered, address expected);
2323
error InvalidCallType(bytes1 callType, bytes1 expected);
24+
error InvalidExecType(bytes1 execType);
2425
error InvalidTopLevelSelector(bytes4 selector, bytes4 expected);
26+
error InvalidData();
2527
error SessionAlreadyExists(bytes32 sessionHash);
2628
error UnlimitedFees();
2729
error SessionExpiresTooSoon(uint256 expiresAt);
2830
error SessionNotActive();
31+
error EmptyTimeRange();
2932
error LifetimeUsageExceeded(uint256 lifetimeUsage, uint256 maxUsage);
3033
error AllowanceExceeded(uint256 allowance, uint256 maxAllowance, uint64 period);
31-
error InvalidDataLength(uint256 actualLength, uint256 expectedMinimumLength);
34+
error InvalidDataLength(uint256 actualLength, uint256 expectedLength);
3235
error ConditionViolated(bytes32 param, bytes32 refValue, uint8 condition);
3336
error CallPolicyViolated(address target, bytes4 selector);
3437
error TransferPolicyViolated(address target);
@@ -159,24 +162,24 @@ library SessionLib {
159162
/// @dev Reverts if the limit is exceeded or the period is invalid.
160163
function checkAndUpdate(UsageLimit memory limit, UsageTracker storage tracker, uint256 value, uint48 period)
161164
internal
162-
returns (uint48 validAfter, uint48 validUntil)
165+
returns (uint48[2] memory validTimeRange)
163166
{
167+
validTimeRange = [0, type(uint48).max];
164168
if (limit.limitType == LimitType.Lifetime) {
165-
validAfter = 0;
166-
validUntil = type(uint48).max;
167169
require(
168170
tracker.lifetimeUsage[msg.sender] + value <= limit.limit,
169171
LifetimeUsageExceeded(tracker.lifetimeUsage[msg.sender], limit.limit)
170172
);
171173
tracker.lifetimeUsage[msg.sender] += value;
172174
} else if (limit.limitType == LimitType.Allowance) {
173-
validAfter = period * limit.period;
174-
validUntil = (period + 1) * limit.period;
175175
require(
176176
tracker.allowanceUsage[period][msg.sender] + value <= limit.limit,
177177
AllowanceExceeded(tracker.allowanceUsage[period][msg.sender], limit.limit, period)
178178
);
179179
tracker.allowanceUsage[period][msg.sender] += value;
180+
uint48 validAfter = period * limit.period;
181+
uint48 validUntil = (period + 1) * limit.period - 1;
182+
validTimeRange = [validAfter, validUntil];
180183
}
181184
}
182185

@@ -192,7 +195,7 @@ library SessionLib {
192195
UsageTracker storage tracker,
193196
bytes memory data,
194197
uint48 period
195-
) internal returns (uint48, uint48) {
198+
) internal returns (uint48[2] memory validTimeRange) {
196199
uint256 expectedLength = 4 + constraint.index * 32 + 32;
197200
if (data.length < expectedLength) {
198201
revert InvalidDataLength(data.length, expectedLength);
@@ -237,22 +240,34 @@ library SessionLib {
237240
PackedUserOperation calldata userOp,
238241
SessionSpec memory spec,
239242
uint48 periodId
240-
) internal returns (uint48 validAfter, uint48 validUntil) {
243+
) internal returns (uint48[2] memory validTimeRange) {
241244
// If a paymaster is paying the fee, we don't need to check the fee limit
242245
if (userOp.paymasterAndData.length == 0) {
243246
uint256 gasPrice = userOp.gasPrice();
244-
uint256 gasLimit = userOp.unpackVerificationGasLimit() + userOp.unpackCallGasLimit();
247+
uint256 gasLimit =
248+
userOp.preVerificationGas + userOp.unpackVerificationGasLimit() + userOp.unpackCallGasLimit();
245249
uint256 fee = gasPrice * gasLimit;
246250
// slither-disable-next-line unused-return
247251
return spec.feeLimit.checkAndUpdate(state.fee, fee, periodId);
248252
} else {
249-
return (0, type(uint48).max);
253+
return [0, type(uint48).max];
250254
}
251255
}
252256

253-
function shrinkRange(uint48[2] memory range, uint48 newAfter, uint48 newUntil) internal pure {
254-
range[0] = newAfter > range[0] ? newAfter : range[0];
255-
range[1] = newUntil < range[1] ? newUntil : range[1];
257+
/// @notice Shrinks the time range to the intersection of itself and the new range.
258+
/// @param range The original time range to shrink: validAfter, validUntil
259+
/// @param otherRange The new time range to intersect with: newValidAfter, newValidUntil
260+
/// @return newRange The shrunk time range: validAfter, validUntil
261+
function shrinkRange(uint48[2] memory range, uint48[2] memory otherRange)
262+
internal
263+
pure
264+
returns (uint48[2] memory newRange)
265+
{
266+
newRange = [
267+
otherRange[0] > range[0] ? otherRange[0] : range[0], // max(validAfter, newValidAfter)
268+
otherRange[1] < range[1] ? otherRange[1] : range[1] // min(validUntil, newValidUntil)
269+
];
270+
require(newRange[0] <= newRange[1], EmptyTimeRange());
256271
}
257272

258273
/// @notice Validates the transaction against the session spec and updates the usage trackers.
@@ -272,13 +287,17 @@ library SessionLib {
272287
PackedUserOperation calldata userOp,
273288
SessionSpec memory spec,
274289
uint48[] memory periodIds
275-
) internal returns (uint48, uint48) {
290+
) internal returns (uint48[2] memory validTimeRange) {
276291
require(state.status[msg.sender] == Status.Active, SessionNotActive());
277292

278293
bytes4 topLevelSelector = bytes4(userOp.callData[:4]);
279294
bytes1 callType = userOp.callData[4];
295+
bytes1 execType = userOp.callData[5];
280296

281297
require(callType == LibERC7579.CALLTYPE_SINGLE, InvalidCallType(callType, LibERC7579.CALLTYPE_SINGLE));
298+
require(
299+
execType == LibERC7579.EXECTYPE_DEFAULT || execType == LibERC7579.EXECTYPE_TRY, InvalidExecType(execType)
300+
);
282301
require(
283302
topLevelSelector == IERC7579Account.execute.selector,
284303
InvalidTopLevelSelector(topLevelSelector, IERC7579Account.execute.selector)
@@ -288,15 +307,20 @@ library SessionLib {
288307
// - first 4 bytes: selector
289308
// - next 32 bytes: mode
290309
// - next 32 bytes: data offset
291-
// - at offset: data length
310+
// - next 32 bytes: data length
292311
// - next 32 bytes: data
293-
uint256 offset = uint256(bytes32(userOp.callData[36:68])) + 4; // offset does not include the selector
294-
uint256 length = uint256(bytes32(userOp.callData[offset:offset + 32]));
312+
uint256 offsetOffset = 4 + 32;
313+
uint256 lengthOffset = 4 + 32 + 32;
314+
uint256 dataOffset = 4 + 32 + 32 + 32;
315+
// Offset does not include the selector, hence +4
316+
uint256 offset = uint256(bytes32(userOp.callData[offsetOffset:offsetOffset + 32])) + 4;
317+
require(offset == lengthOffset, InvalidData());
318+
uint256 length = uint256(bytes32(userOp.callData[lengthOffset:lengthOffset + 32]));
295319
(address target, uint256 value, bytes calldata callData) =
296-
LibERC7579.decodeSingle(userOp.callData[offset + 32:offset + 32 + length]);
320+
LibERC7579.decodeSingle(userOp.callData[dataOffset:dataOffset + length]);
297321

298322
// Time range within which the transaction is valid.
299-
uint48[2] memory timeRange = [0, spec.expiresAt];
323+
validTimeRange = [0, spec.expiresAt];
300324

301325
if (callData.length >= 4) {
302326
bytes4 selector = bytes4(callData[:4]);
@@ -313,17 +337,21 @@ library SessionLib {
313337

314338
require(found, CallPolicyViolated(target, selector));
315339
require(value <= callPolicy.maxValuePerUse, MaxValueExceeded(value, callPolicy.maxValuePerUse));
316-
(uint48 newValidAfter, uint48 newValidUntil) =
317-
callPolicy.valueLimit.checkAndUpdate(state.callValue[target][selector], value, periodIds[1]);
318-
shrinkRange(timeRange, newValidAfter, newValidUntil);
340+
validTimeRange = shrinkRange(
341+
validTimeRange,
342+
callPolicy.valueLimit.checkAndUpdate(state.callValue[target][selector], value, periodIds[1])
343+
);
319344

320345
for (uint256 i = 0; i < callPolicy.constraints.length; ++i) {
321-
(newValidAfter, newValidUntil) = callPolicy.constraints[i].checkAndUpdate(
322-
state.params[target][selector][i], callData, periodIds[2 + i]
346+
validTimeRange = shrinkRange(
347+
validTimeRange,
348+
callPolicy.constraints[i].checkAndUpdate(
349+
state.params[target][selector][i], callData, periodIds[2 + i]
350+
)
323351
);
324-
shrinkRange(timeRange, newValidAfter, newValidUntil);
325352
}
326353
} else {
354+
require(callData.length == 0, InvalidDataLength(callData.length, 0));
327355
TransferSpec memory transferPolicy;
328356
bool found = false;
329357

@@ -337,12 +365,11 @@ library SessionLib {
337365

338366
require(found, TransferPolicyViolated(target));
339367
require(value <= transferPolicy.maxValuePerUse, MaxValueExceeded(value, transferPolicy.maxValuePerUse));
340-
(uint48 newValidAfter, uint48 newValidUntil) =
341-
transferPolicy.valueLimit.checkAndUpdate(state.transferValue[target], value, periodIds[1]);
342-
shrinkRange(timeRange, newValidAfter, newValidUntil);
368+
validTimeRange = shrinkRange(
369+
validTimeRange,
370+
transferPolicy.valueLimit.checkAndUpdate(state.transferValue[target], value, periodIds[1])
371+
);
343372
}
344-
345-
return (timeRange[0], timeRange[1]);
346373
}
347374

348375
/// @notice Getter for the remainder of a usage limit.

0 commit comments

Comments
 (0)