Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/modern-taxes-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`AccountERC7579`: Fallback to native validation if module validation fails
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Narrow the changeset wording to the implemented cases.

This only falls back when no installed validator handles the signature or the validator call reverts; an installed validator returning 0xffffffff still short-circuits as invalid. The current note reads broader than the implementation.

✏️ Suggested wording
-`AccountERC7579`: Fallback to native validation if module validation fails
+`AccountERC7579`: Fallback to native validation when module validation is unavailable or reverts
📝 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
`AccountERC7579`: Fallback to native validation if module validation fails
`AccountERC7579`: Fallback to native validation when module validation is unavailable or reverts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/modern-taxes-exist.md at line 5, Update the changeset text for
`AccountERC7579` to precisely describe the implemented behavior: state that the
contract falls back to native signature validation only when no installed
validator handles the signature or when a validator call reverts, and explicitly
note that an installed validator returning `0xffffffff` still short-circuits and
is treated as invalid; replace the broad phrase "if module validation fails"
with this narrower, exact behavior.

2 changes: 1 addition & 1 deletion contracts/account/extensions/draft-AccountERC7579.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
} catch {}
}
}
return bytes4(0xffffffff);
return _rawSignatureValidation(hash, signature) ? IERC1271.isValidSignature.selector : bytes4(0xffffffff);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would love to check with @ernestognw if that was not by design.

Basically, if we do that we don't have 7739's security, and that could be unsafe. Lets discuss that.

}

/**
Expand Down
13 changes: 13 additions & 0 deletions contracts/mocks/account/AccountMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ abstract contract AccountEIP7702WithModulesMock is
}
}

abstract contract AccountERC7579NativeValidationMock is AccountERC7579, SignerECDSA {
constructor(address signerAddr) SignerECDSA(signerAddr) {
// no validator module
}

function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override(AccountERC7579, SignerECDSA) returns (bool) {
return super._rawSignatureValidation(hash, signature);
}
}

abstract contract AccountERC7579Mock is AccountERC7579 {
constructor(address validator, bytes memory initData) {
_installModule(MODULE_TYPE_VALIDATOR, validator, initData);
Expand Down
66 changes: 36 additions & 30 deletions test/account/extensions/AccountERC7579.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,58 @@ const { shouldBehaveLikeAccountCore } = require('../Account.behavior');
const { shouldBehaveLikeAccountERC7579 } = require('./AccountERC7579.behavior');
const { shouldBehaveLikeERC1271 } = require('../../utils/cryptography/ERC1271.behavior');

async function fixture() {
const fixtureWithValidator = () => fixture(true); // parameters not supported by `loadFixture`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is problematique. lamdba function like that are not properly supported by loadFixture. It ends up re-running everytime, which defeats the purpose of having a fixture.

const fixtureWithoutValidator = () => fixture(false);

async function fixture(withValidator) {
// EOAs and environment
const [other] = await ethers.getSigners();
const target = await ethers.deployContract('CallReceiverMock');
const anotherTarget = await ethers.deployContract('CallReceiverMock');

// ERC-7579 validator
const validator = await ethers.deployContract('$ERC7579ValidatorMock');
const validator = withValidator ? await ethers.deployContract('$ERC7579ValidatorMock') : null;

// ERC-4337 signer
const signer = ethers.Wallet.createRandom();

// ERC-4337 account
const helper = new ERC4337Helper();
const mock = await helper.newAccount('$AccountERC7579Mock', [
validator,
ethers.solidityPacked(['address'], [signer.address]),
]);
const mock = await helper.newAccount(
validator ? '$AccountERC7579Mock' : '$AccountERC7579NativeValidationMock',
validator ? [validator, ethers.solidityPacked(['address'], [signer.address])] : [signer.address],
);

// ERC-4337 Entrypoint domain
const entrypointDomain = await getDomain(predeploy.entrypoint.v09);

return { helper, validator, mock, entrypointDomain, signer, target, anotherTarget, other };
}

describe('AccountERC7579', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));

this.signer.signMessage = message =>
ethers.Wallet.prototype.signMessage
.bind(this.signer)(message)
.then(sign => ethers.concat([this.validator.target, sign]));
this.signer.signTypedData = (domain, types, values) =>
ethers.Wallet.prototype.signTypedData
.bind(this.signer)(domain, types, values)
.then(sign => ethers.concat([this.validator.target, sign]));
this.signUserOp = userOp =>
ethers.Wallet.prototype.signTypedData
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
.then(signature => Object.assign(userOp, { signature }));

this.userOp = { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) };
});

shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountERC7579();
shouldBehaveLikeERC1271();
});
[true, false].forEach(withValidator =>
describe(`AccountERC7579 ${withValidator ? '' : 'native signer validation fallback'}`, function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(withValidator ? fixtureWithValidator : fixtureWithoutValidator));
this.signer.signMessage = message =>
ethers.Wallet.prototype.signMessage
.bind(this.signer)(message)
.then(sign => (withValidator ? ethers.concat([this.validator.target, sign]) : sign));
this.signer.signTypedData = (domain, types, values) =>
ethers.Wallet.prototype.signTypedData
.bind(this.signer)(domain, types, values)
.then(sign => (withValidator ? ethers.concat([this.validator.target, sign]) : sign));
this.signUserOp = userOp =>
ethers.Wallet.prototype.signTypedData
.bind(this.signer)(this.entrypointDomain, { PackedUserOperation }, userOp.packed)
.then(signature => Object.assign(userOp, { signature }));

this.userOp = withValidator ? { nonce: ethers.zeroPadBytes(ethers.hexlify(this.validator.target), 32) } : {};
});

if (withValidator) {
shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountERC7579();
}
shouldBehaveLikeERC1271();
}),
);
Loading