Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion certora/specs/Account.spec
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ invariant absentExecutorIsNotStored(address module, uint256 index)
*/
// This guarantees that at most one fallback module is active for a given initData (i.e. selector)
rule fallbackModule(address module, bytes initData) {
assert isModuleInstalled(FALLBACK_TYPE(), module, initData) <=> getFallbackHandler(getDataSelector(initData)) == module;
assert isModuleInstalled(FALLBACK_TYPE(), module, initData) <=> (initData.length >= 4 && getFallbackHandler(getDataSelector(initData)) == module);
}

rule moduleManagementRule(env e, method f, calldataarg args, uint256 moduleTypeId, address module, bytes additionalContext)
Expand Down
10 changes: 9 additions & 1 deletion contracts/account/extensions/draft-AccountERC7579.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
/// @dev The account's {fallback} was called with a selector that doesn't have an installed handler.
error ERC7579MissingFallbackHandler(bytes4 selector);

/// @dev The provided initData/deInitData for a fallback module is too short to extract a selector.
error ERC7579CannotDecodeFallbackData();

/// @dev The provided signature is not long enough to be parsed as a module signature.
error ERC7579InvalidModuleSignature();

/// @dev Modifier that checks if the caller is an installed module of the given type.
modifier onlyModule(uint256 moduleTypeId, bytes calldata additionalContext) {
_checkModule(moduleTypeId, msg.sender, additionalContext);
Expand Down Expand Up @@ -384,7 +390,8 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
function _extractSignatureValidator(
bytes calldata signature
) internal pure virtual returns (address module, bytes calldata innerSignature) {
return (address(bytes20(signature[0:20])), signature[20:]);
require(signature.length > 19, ERC7579InvalidModuleSignature());
return (address(bytes20(signature)), signature[20:]);
Copy link
Member

Choose a reason for hiding this comment

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

I just realized that isValidSignature is already avoiding that this function is called with a signature that's less than 20 bytes long:

function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
    if (signature.length >= 20) {
                (address module, bytes calldata innerSignature) = _extractSignatureValidator(signature);
               ...

I would suggest reverting this change and just noting that the function does expect a signature of at least 20 bytes. Otherwise this will add an extra (and arguably unnecessary) check.

}

/**
Expand All @@ -399,6 +406,7 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
function _decodeFallbackData(
bytes memory data
) internal pure virtual returns (bytes4 selector, bytes memory remaining) {
require(data.length > 3, ERC7579CannotDecodeFallbackData());
return (bytes4(data), data.slice(4));
}

Expand Down
22 changes: 22 additions & 0 deletions test/account/extensions/AccountERC7579.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
});
}

it('should revert when installing a fallback module with an initData that is not long enough to encode a function selector', async function () {
const instance = this.modules[MODULE_TYPE_FALLBACK];
await expect(
this.mockFromEntrypoint.installModule(MODULE_TYPE_FALLBACK, instance, '0x'),
).to.be.revertedWithCustomError(this.mock, 'ERC7579CannotDecodeFallbackData');

await expect(
this.mockFromEntrypoint.installModule(MODULE_TYPE_FALLBACK, instance, '0x123456'),
).to.be.revertedWithCustomError(this.mock, 'ERC7579CannotDecodeFallbackData');
});

withHooks &&
describe('with hook', function () {
beforeEach(async function () {
Expand Down Expand Up @@ -240,6 +251,17 @@ function shouldBehaveLikeAccountERC7579({ withHooks = false } = {}) {
});
}

it('should revert when uninstalling a fallback module with an initData that is not long enough to encode a function selector', async function () {
const instance = this.modules[MODULE_TYPE_FALLBACK];
await expect(
this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, instance, '0x'),
).to.be.revertedWithCustomError(this.mock, 'ERC7579CannotDecodeFallbackData');

await expect(
this.mockFromEntrypoint.uninstallModule(MODULE_TYPE_FALLBACK, instance, '0x123456'),
).to.be.revertedWithCustomError(this.mock, 'ERC7579CannotDecodeFallbackData');
});

it('should revert uninstalling a module of type MODULE_TYPE_FALLBACK if a different module was installed for the provided selector', async function () {
const instance = this.modules[MODULE_TYPE_FALLBACK];
const anotherInstance = await ethers.deployContract('$ERC7579ModuleMock', [MODULE_TYPE_FALLBACK]);
Expand Down
Loading