Skip to content

Commit c205b41

Browse files
ernestognwAmxx
andauthored
Add ERC-4337 and ERC-7702 account implementations (#25)
* WIP: Migrate Account code * Checkpoint * Fix lint * Checkpoint * up * up * Adjust * up * Simplify CallReceiverMock * Fix slither + Codespell * Fix coverage * Remove entrypoint * Readd entrypoint * Run --ir-minimum in forge coverage * up * Make Accounts initializable * Finish docs * rewrite helpers/signers as alternative to ethers.SigningKey and ethers.BaseWallet * Rename _validateNestedEIP712Signature -> _validateSignature * Read virtual to ERC7739Signer functions * lint * Implement review recommendations * Include signer into account factory hash * Update Account inheritance order * up * Remove ERC1155HolderLean * Abstract AccountSignerDomain * up * Remove signed hash fn * Add standalone example of usage * Remove docs * ERC4337 userOp validation should not be 7739 wrapped * documentation * Rename `_validateSignature` to `_rawSignatureValidation` and remove _validateUserOp * errata * Default _signableUserOpHash to a typed userop signature * Remove docs mocks * Remove ERC7739 from AccountBase * Make ERC7739Signer validations private * Move EIP712 userop signing to Accountbase * Split AccountCore / Account * remove intermediary variable * doc * spelling * abstract signer * docs * ERC7702 signer * fix * doc example for ERC7739 use signers * Complete minimal documentation * Update CHANGELOG.md Co-authored-by: Hadrien Croubois <[email protected]> --------- Co-authored-by: Hadrien Croubois <[email protected]>
1 parent 945884c commit c205b41

39 files changed

+1601
-187
lines changed

.solcover.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
module.exports = {
22
skipFiles: ['mocks'],
33
istanbulReporter: ['html', 'lcov', 'text-summary'],
4+
// Work around stack too deep for coverage
5+
configureYulOptimizer: true,
6+
solcOptimizerDetails: {
7+
yul: true,
8+
yulDetails: {
9+
optimizerSteps: '',
10+
},
11+
},
412
};

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## XX-XX-2024
2+
3+
- `AccountCore`: Added a simple ERC-4337 account implementation with minimal logic to process user operations.
4+
- `Account`: Extensions of {AccountCore} with recommended features that most accounts should have.
5+
- `AbstractSigner`, `SignerECDSA`, `SignerP256`, and `SignerRSA`: Add an abstract contract, and various implementations, for contracts that deal with signature verification. Used by {AccountCore} and {ERC7739Utils}.
6+
- `AccountSignerERC7702`: Implementation of `AbstractSigner` for ERC-7702 compatible accounts.
7+
18
## 06-11-2024
29

310
- `ERC7739Utils`: Add a library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on the ERC-7739.

contracts/account/README.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
= Account
2+
[.readme-notice]
3+
NOTE: This document is better viewed at https://docs.openzeppelin.com/community-contracts/api/account
4+
5+
This directory includes contracts to build accounts for ERC-4337.
6+
7+
== Core
8+
9+
{{AccountCore}}
10+
11+
{{Account}}
12+
13+
== Extensions
14+
15+
{{AccountSignerERC7702}}

contracts/account/draft-Account.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
6+
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
7+
import {ERC7739Signer} from "../utils/cryptography/draft-ERC7739Signer.sol";
8+
import {AccountCore} from "./draft-AccountCore.sol";
9+
10+
/**
11+
* @dev Extension of {AccountCore} with recommended feature that most account abstraction implementation will want:
12+
*
13+
* * {ERC721Holder} and {ERC1155Holder} to accept ERC-712 and ERC-1155 token transfers transfers.
14+
* * {ERC7739Signer} for ERC-1271 signature support with ERC-7739 replay protection
15+
*
16+
* NOTE: To use this contract, the {ERC7739Signer-_rawSignatureValidation} function must be
17+
* implemented using a specific signature verification algorithm. See {SignerECDSA}, {SignerP256} or {SignerRSA}.
18+
*/
19+
abstract contract Account is AccountCore, ERC721Holder, ERC1155Holder, ERC7739Signer {}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {PackedUserOperation, IAccount, IEntryPoint, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
6+
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";
10+
import {AbstractSigner} from "../utils/cryptography/AbstractSigner.sol";
11+
12+
/**
13+
* @dev A simple ERC4337 account implementation. This base implementation only includes the minimal logic to process
14+
* user operations.
15+
*
16+
* Developers must implement the {AbstractSigner-_rawSignatureValidation} function to define the account's validation logic.
17+
*
18+
* IMPORTANT: Implementing a mechanism to validate signatures is a security-sensitive operation as it may allow an
19+
* attacker to bypass the account's security measures. Check out {SignerECDSA}, {SignerP256}, or {SignerRSA} for
20+
* digital signature validation implementations.
21+
*/
22+
abstract contract AccountCore is AbstractSigner, EIP712, IAccount, IAccountExecute {
23+
using MessageHashUtils for bytes32;
24+
25+
bytes32 internal constant _PACKED_USER_OPERATION =
26+
keccak256(
27+
"PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,address entrypoint)"
28+
);
29+
30+
/**
31+
* @dev Unauthorized call to the account.
32+
*/
33+
error AccountUnauthorized(address sender);
34+
35+
/**
36+
* @dev Revert if the caller is not the entry point or the account itself.
37+
*/
38+
modifier onlyEntryPointOrSelf() {
39+
_checkEntryPointOrSelf();
40+
_;
41+
}
42+
43+
/**
44+
* @dev Revert if the caller is not the entry point.
45+
*/
46+
modifier onlyEntryPoint() {
47+
_checkEntryPoint();
48+
_;
49+
}
50+
51+
/**
52+
* @dev Canonical entry point for the account that forwards and validates user operations.
53+
*/
54+
function entryPoint() public view virtual returns (IEntryPoint) {
55+
return IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
56+
}
57+
58+
/**
59+
* @dev Return the account nonce for the canonical sequence.
60+
*/
61+
function getNonce() public view virtual returns (uint256) {
62+
return getNonce(0);
63+
}
64+
65+
/**
66+
* @dev Return the account nonce for a given sequence (key).
67+
*/
68+
function getNonce(uint192 key) public view virtual returns (uint256) {
69+
return entryPoint().getNonce(address(this), key);
70+
}
71+
72+
/**
73+
* @inheritdoc IAccount
74+
*/
75+
function validateUserOp(
76+
PackedUserOperation calldata userOp,
77+
bytes32 userOpHash,
78+
uint256 missingAccountFunds
79+
) public virtual onlyEntryPoint returns (uint256) {
80+
uint256 validationData = _rawSignatureValidation(_signableUserOpHash(userOp, userOpHash), userOp.signature)
81+
? ERC4337Utils.SIG_VALIDATION_SUCCESS
82+
: ERC4337Utils.SIG_VALIDATION_FAILED;
83+
_payPrefund(missingAccountFunds);
84+
return validationData;
85+
}
86+
87+
/**
88+
* @inheritdoc IAccountExecute
89+
*/
90+
function executeUserOp(
91+
PackedUserOperation calldata userOp,
92+
bytes32 /*userOpHash*/
93+
) public virtual onlyEntryPointOrSelf {
94+
(address target, uint256 value, bytes memory data) = abi.decode(userOp.callData[4:], (address, uint256, bytes));
95+
Address.functionCallWithValue(target, data, value);
96+
}
97+
98+
/**
99+
* @dev Returns the digest used by an offchain signer instead of the opaque `userOpHash`.
100+
*
101+
* Given the `userOpHash` calculation is defined by ERC-4337, offchain signers
102+
* may need to sign again this hash by rehashing it with other schemes (e.g. ERC-191).
103+
*
104+
* Returns a typehash following EIP-712 typed data hashing for readability.
105+
*/
106+
function _signableUserOpHash(
107+
PackedUserOperation calldata userOp,
108+
bytes32 /* userOpHash */
109+
) internal view virtual returns (bytes32) {
110+
return
111+
_hashTypedDataV4(
112+
keccak256(
113+
abi.encode(
114+
_PACKED_USER_OPERATION,
115+
userOp.sender,
116+
userOp.nonce,
117+
keccak256(userOp.initCode),
118+
keccak256(userOp.callData),
119+
userOp.accountGasLimits,
120+
userOp.preVerificationGas,
121+
userOp.gasFees,
122+
keccak256(userOp.paymasterAndData),
123+
entryPoint()
124+
)
125+
)
126+
);
127+
}
128+
129+
/**
130+
* @dev Sends the missing funds for executing the user operation to the {entrypoint}.
131+
* The `missingAccountFunds` must be defined by the entrypoint when calling {validateUserOp}.
132+
*/
133+
function _payPrefund(uint256 missingAccountFunds) internal virtual {
134+
if (missingAccountFunds > 0) {
135+
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
136+
success; // Silence warning. The entrypoint should validate the result.
137+
}
138+
}
139+
140+
/**
141+
* @dev Ensures the caller is the {entrypoint}.
142+
*/
143+
function _checkEntryPoint() internal view virtual {
144+
address sender = msg.sender;
145+
if (sender != address(entryPoint())) {
146+
revert AccountUnauthorized(sender);
147+
}
148+
}
149+
150+
/**
151+
* @dev Ensures the caller is the {entrypoint} or the account itself.
152+
*/
153+
function _checkEntryPointOrSelf() internal view virtual {
154+
address sender = msg.sender;
155+
if (sender != address(this) && sender != address(entryPoint())) {
156+
revert AccountUnauthorized(sender);
157+
}
158+
}
159+
160+
/**
161+
* @dev Receive Ether.
162+
*/
163+
receive() external payable virtual {}
164+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import {AccountCore} from "../draft-AccountCore.sol";
7+
8+
/**
9+
* @dev {Account} implementation whose low-level signature validation is done by an EOA.
10+
*/
11+
abstract contract AccountSignerERC7702 is AccountCore {
12+
/**
13+
* @dev Validates the signature using the EOA's address (ie. `address(this)`).
14+
*/
15+
function _rawSignatureValidation(
16+
bytes32 hash,
17+
bytes calldata signature
18+
) internal view virtual override returns (bool) {
19+
(address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
20+
return address(this) == recovered && err == ECDSA.RecoverError.NoError;
21+
}
22+
}

contracts/mocks/CallReceiverMock.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol";
6+
7+
contract CallReceiverMockExtended is CallReceiverMock {
8+
event MockFunctionCalledExtra(address caller, uint256 value);
9+
10+
function mockFunctionExtra() public payable {
11+
emit MockFunctionCalledExtra(msg.sender, msg.value);
12+
}
13+
}

contracts/mocks/Create2Mock.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
6+
7+
contract Create2Mock {
8+
function $deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external returns (address) {
9+
return Create2.deploy(amount, salt, bytecode);
10+
}
11+
12+
function $computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address) {
13+
return Create2.computeAddress(salt, bytecodeHash, address(this));
14+
}
15+
16+
function $computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) external pure returns (address) {
17+
return Create2.computeAddress(salt, bytecodeHash, deployer);
18+
}
19+
}

contracts/mocks/ERC1155Mock.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
6+
7+
abstract contract ERC1155Mock is ERC1155 {}

contracts/mocks/ERC721Mock.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
6+
7+
abstract contract ERC721Mock is ERC721 {}

0 commit comments

Comments
 (0)