Skip to content

Commit 2bda8f9

Browse files
authored
AA-521 EntryPoint support for eip-7702 (eth-infinitism#529)
1 parent d8b8076 commit 2bda8f9

21 files changed

+841
-101
lines changed

.solcover.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module.exports = {
22
skipFiles: [
33
"test",
4-
"samples/bls/lib",
5-
"utils/Exec.sol"
4+
"utils/Exec.sol",
5+
"samples"
66
],
77
configureYulOptimizer: true,
88
};

contracts/core/Eip7702Support.sol

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
pragma solidity ^0.8;
2+
// SPDX-License-Identifier: MIT
3+
// solhint-disable no-inline-assembly
4+
5+
import "../interfaces/PackedUserOperation.sol";
6+
import "../core/UserOperationLib.sol";
7+
8+
library Eip7702Support {
9+
10+
// EIP-7702 code prefix before delegate address.
11+
bytes3 internal constant EIP7702_PREFIX = 0xef0100;
12+
13+
// EIP-7702 initCode marker, to specify this account is EIP-7702.
14+
bytes2 internal constant INITCODE_EIP7702_MARKER = 0x7702;
15+
16+
using UserOperationLib for PackedUserOperation;
17+
18+
//get alternate InitCodeHash (just for UserOp hash) when using EIP-7702
19+
function _getEip7702InitCodeHashOverride(PackedUserOperation calldata userOp) internal view returns (bytes32) {
20+
bytes calldata initCode = userOp.initCode;
21+
if (!_isEip7702InitCode(initCode)) {
22+
return 0;
23+
}
24+
address delegate = _getEip7702Delegate(userOp.getSender());
25+
if (initCode.length <= 20)
26+
return keccak256(abi.encodePacked(delegate));
27+
else
28+
return keccak256(abi.encodePacked(delegate, initCode[20 :]));
29+
}
30+
31+
// check if this initCode is EIP-7702: starts with INITCODE_EIP7702_MARKER.
32+
function _isEip7702InitCode(bytes calldata initCode) internal pure returns (bool) {
33+
34+
if (initCode.length < 2) {
35+
return false;
36+
}
37+
bytes20 initCodeStart;
38+
// non-empty calldata bytes are always zero-padded to 32-bytes, so can be safely casted to "bytes20"
39+
assembly ("memory-safe") {
40+
initCodeStart := calldataload(initCode.offset)
41+
}
42+
// make sure first 20 bytes of initCode are "0x7702" (padded with zeros)
43+
return initCodeStart == bytes20(INITCODE_EIP7702_MARKER);
44+
}
45+
46+
/**
47+
* get the EIP-7702 delegate from contract code.
48+
* must only be used if _isEip7702InitCode(initCode) is true.
49+
*/
50+
function _getEip7702Delegate(address sender) internal view returns (address) {
51+
52+
bytes32 senderCode;
53+
54+
assembly ("memory-safe") {
55+
extcodecopy(sender, 0, 0, 23)
56+
senderCode := mload(0)
57+
}
58+
// To be a valid EIP-7702 delegate, the first 3 bytes are EIP7702_PREFIX
59+
// followed by the delegate address
60+
if (bytes3(senderCode) != EIP7702_PREFIX) {
61+
// instead of just "not an EIP-7702 delegate", if some info.
62+
require(sender.code.length > 0, "sender has no code");
63+
revert("not an EIP-7702 delegate");
64+
}
65+
return address(bytes20(senderCode << 24));
66+
}
67+
}

contracts/core/EntryPoint.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import "./NonceManager.sol";
1414
import "./SenderCreator.sol";
1515
import "./StakeManager.sol";
1616
import "./UserOperationLib.sol";
17+
import "./Eip7702Support.sol";
1718

1819
import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
1920
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
@@ -381,8 +382,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
381382
function getUserOpHash(
382383
PackedUserOperation calldata userOp
383384
) public view returns (bytes32) {
385+
bytes32 overrideInitCodeHash = Eip7702Support._getEip7702InitCodeHashOverride(userOp);
384386
return
385-
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash());
387+
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash));
386388
}
387389

388390
/**
@@ -444,6 +446,13 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
444446
) internal {
445447
if (initCode.length != 0) {
446448
address sender = opInfo.mUserOp.sender;
449+
if ( Eip7702Support._isEip7702InitCode(initCode) ) {
450+
if (initCode.length>20 ) {
451+
//already validated it is an EIP-7702 delegate (and hence, already has code)
452+
senderCreator().initEip7702Sender(sender, initCode[20:]);
453+
}
454+
return;
455+
}
447456
if (sender.code.length != 0)
448457
revert FailedOp(opIndex, "AA10 sender already constructed");
449458
address sender1 = senderCreator().createSender{

contracts/core/EntryPointSimulations.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
215215
return __domainSeparatorV4;
216216
}
217217

218+
function supportsInterface(bytes4) public view virtual override returns (bool) {
219+
return false;
220+
}
218221
}

contracts/core/SenderCreator.sol

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity ^0.8.23;
3+
/* solhint-disable avoid-low-level-calls */
4+
/* solhint-disable no-inline-assembly */
35

46
import "../interfaces/ISenderCreator.sol";
7+
import "../interfaces/IEntryPoint.sol";
8+
import "../utils/Exec.sol";
59

610
/**
711
* Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
@@ -14,6 +18,8 @@ contract SenderCreator is ISenderCreator {
1418
entryPoint = msg.sender;
1519
}
1620

21+
uint256 private constant REVERT_REASON_MAX_LEN = 2048;
22+
1723
/**
1824
* Call the "initCode" factory to create and return the sender account address.
1925
* @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address,
@@ -23,13 +29,11 @@ contract SenderCreator is ISenderCreator {
2329
function createSender(
2430
bytes calldata initCode
2531
) external returns (address sender) {
26-
if (msg.sender != entryPoint) {
27-
revert("AA97 should call from EntryPoint");
28-
}
29-
address factory = address(bytes20(initCode[0:20]));
30-
bytes memory initCallData = initCode[20:];
32+
require(msg.sender == entryPoint, "AA97 should call from EntryPoint");
33+
address factory = address(bytes20(initCode[0 : 20]));
34+
35+
bytes memory initCallData = initCode[20 :];
3136
bool success;
32-
/* solhint-disable no-inline-assembly */
3337
assembly ("memory-safe") {
3438
success := call(
3539
gas(),
@@ -40,10 +44,23 @@ contract SenderCreator is ISenderCreator {
4044
0,
4145
32
4246
)
43-
sender := mload(0)
47+
if success {
48+
sender := mload(0)
49+
}
4450
}
51+
}
52+
53+
// use initCallData to initialize an EIP-7702 account
54+
// caller (EntryPoint) already verified it is an EIP-7702 account.
55+
function initEip7702Sender(
56+
address sender,
57+
bytes calldata initCallData
58+
) external {
59+
require(msg.sender == entryPoint, "AA97 should call from EntryPoint");
60+
bool success = Exec.call(sender, 0, initCallData, gasleft());
4561
if (!success) {
46-
sender = address(0);
62+
bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN);
63+
revert IEntryPoint.FailedOpWithRevert(0, "AA13 EIP7702 sender init failed", result);
4764
}
4865
}
4966
}

contracts/core/UserOperationLib.sol

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ library UserOperationLib {
5555
/**
5656
* Pack the user operation data into bytes for hashing.
5757
* @param userOp - The user operation data.
58+
* @param overrideInitCodeHash - If set, encode this instead of the initCode field in the userOp.
5859
*/
5960
function encode(
60-
PackedUserOperation calldata userOp
61+
PackedUserOperation calldata userOp,
62+
bytes32 overrideInitCodeHash
6163
) internal pure returns (bytes memory ret) {
6264
address sender = getSender(userOp);
6365
uint256 nonce = userOp.nonce;
64-
bytes32 hashInitCode = calldataKeccak(userOp.initCode);
66+
bytes32 hashInitCode = overrideInitCodeHash != 0 ? overrideInitCodeHash : calldataKeccak(userOp.initCode);
6567
bytes32 hashCallData = calldataKeccak(userOp.callData);
6668
bytes32 accountGasLimits = userOp.accountGasLimits;
6769
uint256 preVerificationGas = userOp.preVerificationGas;
@@ -136,10 +138,12 @@ library UserOperationLib {
136138
/**
137139
* Hash the user operation data.
138140
* @param userOp - The user operation data.
141+
* @param overrideInitCodeHash - If set, the initCode hash will be replaced with this value just for UserOp hashing.
139142
*/
140143
function hash(
141-
PackedUserOperation calldata userOp
144+
PackedUserOperation calldata userOp,
145+
bytes32 overrideInitCodeHash
142146
) internal pure returns (bytes32) {
143-
return keccak256(encode(userOp));
147+
return keccak256(encode(userOp, overrideInitCodeHash));
144148
}
145149
}

contracts/interfaces/ISenderCreator.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ interface ISenderCreator {
88
* @return sender Address of the newly created sender contract.
99
*/
1010
function createSender(bytes calldata initCode) external returns (address sender);
11+
12+
// call initCode to initialize an EIP-7702 account
13+
function initEip7702Sender(address sender, bytes calldata initCode) external;
1114
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
pragma solidity ^0.8.23;
2+
// SPDX-License-Identifier: MIT
3+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
4+
import "../core/BaseAccount.sol";
5+
import "../core/Eip7702Support.sol";
6+
7+
contract TestEip7702DelegateAccount is BaseAccount {
8+
9+
IEntryPoint private immutable _entryPoint;
10+
bool public testInitCalled;
11+
12+
constructor(IEntryPoint anEntryPoint) {
13+
_entryPoint = anEntryPoint;
14+
}
15+
16+
function testInit() public {
17+
testInitCalled = true;
18+
}
19+
20+
function entryPoint() public view override virtual returns (IEntryPoint) {
21+
return _entryPoint;
22+
}
23+
24+
// Require the function call went through EntryPoint or owner
25+
function _requireFromEntryPointOrOwner() internal view {
26+
require(msg.sender == address(this) || msg.sender == address(entryPoint()), "account: not Owner or EntryPoint");
27+
}
28+
29+
/**
30+
* execute a transaction (called directly from owner, or by entryPoint)
31+
* @param dest destination address to call
32+
* @param value the value to pass in this call
33+
* @param func the calldata to pass in this call
34+
*/
35+
function execute(address dest, uint256 value, bytes calldata func) external {
36+
_requireFromEntryPointOrOwner();
37+
(bool success,) = dest.call{value: value}(func);
38+
require(success, "call failed");
39+
}
40+
41+
function _validateSignature(
42+
PackedUserOperation calldata userOp,
43+
bytes32 userOpHash
44+
) internal virtual override returns (uint256 validationData) {
45+
if (userOp.initCode.length > 20) {
46+
require(testInitCalled, "testInit not called");
47+
}
48+
if (ECDSA.recover(userOpHash, userOp.signature) == address(this)) {
49+
return 0;
50+
}
51+
return 1;
52+
}
53+
}

contracts/test/TestUtil.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
pragma solidity ^0.8.23;
33

44
import "../interfaces/PackedUserOperation.sol";
5-
import "../core/UserOperationLib.sol";
5+
import "../core/Eip7702Support.sol";
66

77
contract TestUtil {
88
using UserOperationLib for PackedUserOperation;
99

1010
function encodeUserOp(PackedUserOperation calldata op) external pure returns (bytes memory){
11-
return op.encode();
11+
return op.encode(0);
1212
}
1313

14+
function isEip7702InitCode(bytes calldata initCode) external pure returns (bool) {
15+
return Eip7702Support._isEip7702InitCode(initCode);
16+
}
1417
}

reports/gas-checker.txt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,36 @@
1212
║ │ │ │ (delta for │ (compared to ║
1313
║ │ │ │ one UserOp) │ account.exec()) ║
1414
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
15-
║ simple │ 1 │ 77468 │ │ ║
15+
║ simple │ 1 │ 77824 │ │ ║
1616
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
17-
║ simple - diff from previous │ 2 │ │ 4160412345
17+
║ simple - diff from previous │ 2 │ │ 4199612737
1818
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
19-
║ simple │ 10 │ 452160 │ │ ║
19+
║ simple │ 10 │ 455792 │ │ ║
2020
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
21-
║ simple - diff from previous │ 11 │ │ 4167712418
21+
║ simple - diff from previous │ 11 │ │ 4203312774
2222
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
23-
║ simple paymaster │ 1 │ 83307 │ │ ║
23+
║ simple paymaster │ 1 │ 83675 │ │ ║
2424
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
25-
║ simple paymaster with diff │ 2 │ │ 4015610897
25+
║ simple paymaster with diff │ 2 │ │ 4052411265
2626
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
27-
║ simple paymaster │ 10 │ 444914 │ │ ║
27+
║ simple paymaster │ 10 │ 448582 │ │ ║
2828
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
29-
║ simple paymaster with diff │ 11 │ │ 4017710918
29+
║ simple paymaster with diff │ 11 │ │ 4059311334
3030
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
31-
║ big tx 5k │ 1 │ 167233 │ │ ║
31+
║ big tx 5k │ 1 │ 167613 │ │ ║
3232
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
33-
║ big tx - diff from previous │ 2 │ │ 13087116169
33+
║ big tx - diff from previous │ 2 │ │ 13121516513
3434
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
35-
║ big tx 5k │ 10 │ 1345038 │ │ ║
35+
║ big tx 5k │ 10 │ 1348706 │ │ ║
3636
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
37-
║ big tx - diff from previous │ 11 │ │ 13089616194
37+
║ big tx - diff from previous │ 11 │ │ 13122816526
3838
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
39-
║ paymaster+postOp │ 1 │ 84690 │ │ ║
39+
║ paymaster+postOp │ 1 │ 85070 │ │ ║
4040
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
41-
║ paymaster+postOp with diff │ 2 │ │ 4157412315
41+
║ paymaster+postOp with diff │ 2 │ │ 4193012671
4242
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
43-
║ paymaster+postOp │ 10 │ 458869 │ │ ║
43+
║ paymaster+postOp │ 10 │ 462525 │ │ ║
4444
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
45-
║ paymaster+postOp with diff │ 11 │ │ 4157712318
45+
║ paymaster+postOp with diff │ 11 │ │ 4199312734
4646
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝
4747

0 commit comments

Comments
 (0)