Skip to content

Commit c1248a5

Browse files
Amxxernestognw
andauthored
Move EIP712 dependency from AccountCore to Account (#58)
Co-authored-by: Ernesto García <[email protected]>
1 parent 120127b commit c1248a5

File tree

13 files changed

+208
-214
lines changed

13 files changed

+208
-214
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- `AccountCore`: Added a simple ERC-4337 account implementation with minimal logic to process user operations.
1313
- `Account`: Extensions of AccountCore with recommended features that most accounts should have.
1414
- `AbstractSigner`, `SignerECDSA`, `SignerP256`, and `SignerRSA`: Add an abstract contract, and various implementations, for contracts that deal with signature verification. Used by AccountCore and `ERC7739Utils.
15-
- `AccountSignerERC7702`: Implementation of `AbstractSigner` for ERC-7702 compatible accounts.
15+
- `SignerERC7702`: Implementation of `AbstractSigner` for Externally Owned Accounts (EOAs). Useful with ERC-7702.
1616

1717
## 13-12-2024
1818

contracts/account/Account.sol

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
pragma solidity ^0.8.20;
44

5+
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
57
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
68
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
79
import {ERC7739} from "../utils/cryptography/ERC7739.sol";
@@ -18,7 +20,39 @@ import {AccountCore} from "./AccountCore.sol";
1820
* NOTE: To use this contract, the {ERC7739-_rawSignatureValidation} function must be
1921
* implemented using a specific signature verification algorithm. See {SignerECDSA}, {SignerP256} or {SignerRSA}.
2022
*/
21-
abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739, ERC7821 {
23+
abstract contract Account is AccountCore, EIP712, ERC721Holder, ERC1155Holder, ERC7739, ERC7821 {
24+
bytes32 internal constant _PACKED_USER_OPERATION =
25+
keccak256(
26+
"PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)"
27+
);
28+
29+
/**
30+
* @dev Specialization of {AccountCore-_signableUserOpHash} that returns a typehash following EIP-712 typed data
31+
* hashing for readability. This assumes the underlying signature scheme implements `signTypedData`, which will be
32+
* the case when combined with {SignerECDSA} or {SignerERC7702}.
33+
*/
34+
function _signableUserOpHash(
35+
PackedUserOperation calldata userOp,
36+
bytes32 /*userOpHash*/
37+
) internal view virtual override returns (bytes32) {
38+
return
39+
_hashTypedDataV4(
40+
keccak256(
41+
abi.encode(
42+
_PACKED_USER_OPERATION,
43+
userOp.sender,
44+
userOp.nonce,
45+
keccak256(userOp.initCode),
46+
keccak256(userOp.callData),
47+
userOp.accountGasLimits,
48+
userOp.preVerificationGas,
49+
userOp.gasFees,
50+
keccak256(userOp.paymasterAndData)
51+
)
52+
)
53+
);
54+
}
55+
2256
/// @inheritdoc ERC7821
2357
function _erc7821AuthorizedExecutor(
2458
address caller,

contracts/account/AccountCore.sol

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ pragma solidity ^0.8.20;
44

55
import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
66
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
7-
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
8-
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
9-
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
107
import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol";
118

129
/**
1310
* @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
1411
* user operations.
1512
*
16-
* Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic.
13+
* Developers must implement the {AccountCore-_signableUserOpHash} and {AbstractSigner-_rawSignatureValidation}
14+
* functions to define the account's validation logic.
1715
*
1816
* NOTE: This core account doesn't include any mechanism for performing arbitrary external calls. This is an essential
1917
* feature that all Account should have. We leave it up to the developers to implement the mechanism of their choice.
@@ -23,14 +21,7 @@ import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol";
2321
* attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for
2422
* digital signature validation implementations.
2523
*/
26-
abstract contract AccountCore is AbstractSigner, EIP712, IAccount {
27-
using MessageHashUtils for bytes32;
28-
29-
bytes32 internal constant _PACKED_USER_OPERATION =
30-
keccak256(
31-
"PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)"
32-
);
33-
24+
abstract contract AccountCore is AbstractSigner, IAccount {
3425
/**
3526
* @dev Unauthorized call to the account.
3627
*/
@@ -89,34 +80,14 @@ abstract contract AccountCore is AbstractSigner, EIP712, IAccount {
8980
}
9081

9182
/**
92-
* @dev Returns the digest used by an offchain signer instead of the opaque `userOpHash`.
93-
*
94-
* Given the `userOpHash` calculation is defined by ERC-4337, offchain signers
95-
* may need to sign again this hash by rehashing it with other schemes (e.g. ERC-191).
96-
*
97-
* Returns a typehash following EIP-712 typed data hashing for readability.
83+
* @dev Virtual function that returns the signable hash for a user operations. Some implementation may return
84+
* `userOpHash` while other may prefer a signer-friendly value such as an EIP-712 hash describing the `userOp`
85+
* details.
9886
*/
9987
function _signableUserOpHash(
10088
PackedUserOperation calldata userOp,
101-
bytes32 /* userOpHash */
102-
) internal view virtual returns (bytes32) {
103-
return
104-
_hashTypedDataV4(
105-
keccak256(
106-
abi.encode(
107-
_PACKED_USER_OPERATION,
108-
userOp.sender,
109-
userOp.nonce,
110-
keccak256(userOp.initCode),
111-
keccak256(userOp.callData),
112-
userOp.accountGasLimits,
113-
userOp.preVerificationGas,
114-
userOp.gasFees,
115-
keccak256(userOp.paymasterAndData)
116-
)
117-
)
118-
);
119-
}
89+
bytes32 userOpHash
90+
) internal view virtual returns (bytes32);
12091

12192
/**
12293
* @dev Sends the missing funds for executing the user operation to the {entrypoint}.

contracts/account/README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ This directory includes contracts to build accounts for ERC-4337. These include:
1717

1818
== Extensions
1919

20-
{{AccountSignerERC7702}}
20+
{{SignerERC7702}}
2121

2222
{{ERC7821}}

contracts/mocks/account/AccountERC7702Mock.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
pragma solidity ^0.8.20;
44

55
import {Account} from "../../account/Account.sol";
6-
import {AccountSignerERC7702} from "../../account/extensions/AccountSignerERC7702.sol";
6+
import {SignerERC7702} from "../../utils/cryptography/SignerERC7702.sol";
77

8-
abstract contract AccountERC7702Mock is Account, AccountSignerERC7702 {}
8+
abstract contract AccountERC7702Mock is Account, SignerERC7702 {}

contracts/account/extensions/AccountSignerERC7702.sol renamed to contracts/utils/cryptography/SignerERC7702.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
pragma solidity ^0.8.20;
44

55
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6-
import {AccountCore} from "../AccountCore.sol";
6+
import {AbstractSigner} from "./AbstractSigner.sol";
77

88
/**
9-
* @dev {Account} implementation whose low-level signature validation is done by an EOA.
9+
* @dev Implementation of {AbstractSigner} for implementation for an EOA. Useful for ERC-7702 accounts.
1010
*/
11-
abstract contract AccountSignerERC7702 is AccountCore {
11+
abstract contract SignerERC7702 is AbstractSigner {
1212
/**
1313
* @dev Validates the signature using the EOA's address (ie. `address(this)`).
1414
*/

test/account/Account.behavior.js

Lines changed: 0 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const { setBalance } = require('@nomicfoundation/hardhat-network-helpers');
44

55
const { impersonate } = require('@openzeppelin/contracts/test/helpers/account');
66
const { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE } = require('@openzeppelin/contracts/test/helpers/erc4337');
7-
const { CALL_TYPE_BATCH, encodeMode, encodeBatch } = require('@openzeppelin/contracts/test/helpers/erc7579');
87
const {
98
shouldSupportInterfaces,
109
} = require('@openzeppelin/contracts/test/utils/introspection/SupportsInterface.behavior');
@@ -149,145 +148,7 @@ function shouldBehaveLikeAccountHolder() {
149148
});
150149
}
151150

152-
function shouldBehaveLikeAccountERC7821({ deployable = true } = {}) {
153-
describe('execute', function () {
154-
beforeEach(async function () {
155-
// give eth to the account (before deployment)
156-
await setBalance(this.mock.target, ethers.parseEther('1'));
157-
158-
// account is not initially deployed
159-
await expect(ethers.provider.getCode(this.mock)).to.eventually.equal('0x');
160-
161-
this.encodeUserOpCalldata = (...calls) =>
162-
this.mock.interface.encodeFunctionData('execute', [
163-
encodeMode({ callType: CALL_TYPE_BATCH }),
164-
encodeBatch(...calls),
165-
]);
166-
});
167-
168-
it('should revert if the caller is not the canonical entrypoint or the account itself', async function () {
169-
await this.mock.deploy();
170-
171-
await expect(
172-
this.mock.connect(this.other).execute(
173-
encodeMode({ callType: CALL_TYPE_BATCH }),
174-
encodeBatch({
175-
target: this.target,
176-
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
177-
}),
178-
),
179-
)
180-
.to.be.revertedWithCustomError(this.mock, 'AccountUnauthorized')
181-
.withArgs(this.other);
182-
});
183-
184-
if (deployable) {
185-
describe('when not deployed', function () {
186-
it('should be created with handleOps and increase nonce', async function () {
187-
const operation = await this.mock
188-
.createUserOp({
189-
callData: this.encodeUserOpCalldata({
190-
target: this.target,
191-
value: 17,
192-
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
193-
}),
194-
})
195-
.then(op => op.addInitCode())
196-
.then(op => this.signUserOp(op));
197-
198-
// Can't call the account to get its nonce before it's deployed
199-
await expect(entrypoint.getNonce(this.mock.target, 0)).to.eventually.equal(0);
200-
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
201-
.to.emit(entrypoint, 'AccountDeployed')
202-
.withArgs(operation.hash(), this.mock, this.factory, ethers.ZeroAddress)
203-
.to.emit(this.target, 'MockFunctionCalledExtra')
204-
.withArgs(this.mock, 17);
205-
await expect(this.mock.getNonce()).to.eventually.equal(1);
206-
});
207-
208-
it('should revert if the signature is invalid', async function () {
209-
const operation = await this.mock
210-
.createUserOp({
211-
callData: this.encodeUserOpCalldata({
212-
target: this.target,
213-
value: 17,
214-
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
215-
}),
216-
})
217-
.then(op => op.addInitCode());
218-
219-
operation.signature = '0x00';
220-
221-
await expect(entrypoint.handleOps([operation.packed], this.beneficiary)).to.be.reverted;
222-
});
223-
});
224-
}
225-
226-
describe('when deployed', function () {
227-
beforeEach(async function () {
228-
await this.mock.deploy();
229-
});
230-
231-
it('should increase nonce and call target', async function () {
232-
const operation = await this.mock
233-
.createUserOp({
234-
callData: this.encodeUserOpCalldata({
235-
target: this.target,
236-
value: 42,
237-
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
238-
}),
239-
})
240-
.then(op => this.signUserOp(op));
241-
242-
await expect(this.mock.getNonce()).to.eventually.equal(0);
243-
await expect(entrypoint.handleOps([operation.packed], this.beneficiary))
244-
.to.emit(this.target, 'MockFunctionCalledExtra')
245-
.withArgs(this.mock, 42);
246-
await expect(this.mock.getNonce()).to.eventually.equal(1);
247-
});
248-
249-
it('should support sending eth to an EOA', async function () {
250-
const operation = await this.mock
251-
.createUserOp({ callData: this.encodeUserOpCalldata({ target: this.other, value }) })
252-
.then(op => this.signUserOp(op));
253-
254-
await expect(this.mock.getNonce()).to.eventually.equal(0);
255-
await expect(entrypoint.handleOps([operation.packed], this.beneficiary)).to.changeEtherBalance(
256-
this.other,
257-
value,
258-
);
259-
await expect(this.mock.getNonce()).to.eventually.equal(1);
260-
});
261-
262-
it('should support batch execution', async function () {
263-
const value1 = 43374337n;
264-
const value2 = 69420n;
265-
266-
const operation = await this.mock
267-
.createUserOp({
268-
callData: this.encodeUserOpCalldata(
269-
{ target: this.other, value: value1 },
270-
{
271-
target: this.target,
272-
value: value2,
273-
data: this.target.interface.encodeFunctionData('mockFunctionExtra'),
274-
},
275-
),
276-
})
277-
.then(op => this.signUserOp(op));
278-
279-
await expect(this.mock.getNonce()).to.eventually.equal(0);
280-
const tx = entrypoint.handleOps([operation.packed], this.beneficiary);
281-
await expect(tx).to.changeEtherBalances([this.other, this.target], [value1, value2]);
282-
await expect(tx).to.emit(this.target, 'MockFunctionCalledExtra').withArgs(this.mock, value2);
283-
await expect(this.mock.getNonce()).to.eventually.equal(1);
284-
});
285-
});
286-
});
287-
}
288-
289151
module.exports = {
290152
shouldBehaveLikeAccountCore,
291153
shouldBehaveLikeAccountHolder,
292-
shouldBehaveLikeAccountERC7821,
293154
};

test/account/Account.test.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
33
const { ERC4337Helper } = require('../helpers/erc4337');
44
const { NonNativeSigner } = require('../helpers/signers');
55

6-
const {
7-
shouldBehaveLikeAccountCore,
8-
shouldBehaveLikeAccountERC7821,
9-
shouldBehaveLikeAccountHolder,
10-
} = require('./Account.behavior');
6+
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
7+
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
118

129
async function fixture() {
1310
// EOAs and environment
@@ -36,6 +33,6 @@ describe('Account', function () {
3633
});
3734

3835
shouldBehaveLikeAccountCore();
39-
shouldBehaveLikeAccountERC7821();
4036
shouldBehaveLikeAccountHolder();
37+
shouldBehaveLikeERC7821();
4138
});

test/account/AccountECDSA.test.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
33
const { ERC4337Helper } = require('../helpers/erc4337');
44
const { PackedUserOperation } = require('../helpers/eip712-types');
55

6-
const {
7-
shouldBehaveLikeAccountCore,
8-
shouldBehaveLikeAccountERC7821,
9-
shouldBehaveLikeAccountHolder,
10-
} = require('./Account.behavior');
6+
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
117
const { shouldBehaveLikeERC7739 } = require('../utils/cryptography/ERC7739.behavior');
8+
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
129

1310
async function fixture() {
1411
// EOAs and environment
@@ -45,8 +42,8 @@ describe('AccountECDSA', function () {
4542
});
4643

4744
shouldBehaveLikeAccountCore();
48-
shouldBehaveLikeAccountERC7821();
4945
shouldBehaveLikeAccountHolder();
46+
shouldBehaveLikeERC7821();
5047

5148
describe('ERC7739', function () {
5249
beforeEach(async function () {

test/account/AccountERC7702.test.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
33
const { ERC4337Helper } = require('../helpers/erc4337');
44
const { PackedUserOperation } = require('../helpers/eip712-types');
55

6-
const {
7-
shouldBehaveLikeAccountCore,
8-
shouldBehaveLikeAccountERC7821,
9-
shouldBehaveLikeAccountHolder,
10-
} = require('./Account.behavior');
6+
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
117
const { shouldBehaveLikeERC7739 } = require('../utils/cryptography/ERC7739.behavior');
8+
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
129

1310
async function fixture() {
1411
// EOAs and environment
@@ -45,8 +42,8 @@ describe('AccountERC7702', function () {
4542
});
4643

4744
shouldBehaveLikeAccountCore();
48-
shouldBehaveLikeAccountERC7821({ deployable: false });
4945
shouldBehaveLikeAccountHolder();
46+
shouldBehaveLikeERC7821({ deployable: false });
5047

5148
describe('ERC7739', function () {
5249
beforeEach(async function () {

0 commit comments

Comments
 (0)