Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AA-521 EntryPoint support for eip-7702 #529

Open
wants to merge 34 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
db002af
userOpHash as ERC-712 signature
drortirosh Jan 1, 2025
edb2255
don't encode "domain" by default.
drortirosh Jan 2, 2025
dc43285
working
drortirosh Jan 13, 2025
fc0c4e7
removed initUserOpHashParams (internal helper)
drortirosh Jan 13, 2025
263a232
gas cals
drortirosh Jan 13, 2025
5b052ae
undo gas limit change.
drortirosh Jan 13, 2025
38d4714
pr review (typo hash1)
drortirosh Jan 15, 2025
5f0a6b7
remove unused comment.
drortirosh Jan 20, 2025
68894e4
initial implementation
drortirosh Jan 22, 2025
317d26b
memory-safe
drortirosh Jan 22, 2025
2617f03
optimize overrideInitCode
drortirosh Jan 22, 2025
5052317
addeds: zero-tails, fail if not eip-7702 account.
drortirosh Jan 23, 2025
b8a9d26
gas calcs
drortirosh Jan 23, 2025
d9b3f77
lints
drortirosh Jan 23, 2025
24b5473
Merge branch 'develop' into AA-521-ep-7702
drortirosh Jan 23, 2025
fafeca8
removed extracheck.
drortirosh Jan 23, 2025
f98cd95
gascalc
drortirosh Jan 23, 2025
fb06bc3
tests passes, including 7702-enabled external geth
drortirosh Jan 29, 2025
69499d0
gaschecks, geth docker.
drortirosh Jan 29, 2025
bac3113
geth docker.
drortirosh Jan 29, 2025
e54c2a7
fix geth script, coverage test
drortirosh Jan 29, 2025
146dbbe
separate entrypoint-7702 tests into a separate test file
drortirosh Jan 29, 2025
0860e41
Merge branch 'develop' into AA-521-ep-7702
drortirosh Jan 29, 2025
f25a26b
Merge branch 'develop' into AA-521-ep-7702
drortirosh Jan 30, 2025
ee54899
test timeout
drortirosh Jan 30, 2025
33b294a
lints
drortirosh Feb 4, 2025
ea09602
Merge branch 'develop' into AA-521-ep-7702
drortirosh Feb 6, 2025
8dafd94
account
drortirosh Feb 6, 2025
c026b2e
added wallet tests
drortirosh Feb 9, 2025
1442357
test eip-7702 account
drortirosh Feb 9, 2025
b088f05
test 7702 with paymaster
drortirosh Feb 9, 2025
6459a16
lnts
drortirosh Feb 9, 2025
192be39
coverage
drortirosh Feb 10, 2025
26d6a0b
rename to Simple7702Account
drortirosh Feb 10, 2025
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
58 changes: 58 additions & 0 deletions contracts/core/Eip7702Support.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
pragma solidity ^0.8;

import "../interfaces/PackedUserOperation.sol";
import "../core/UserOperationLib.sol";
// SPDX-License-Identifier: MIT

// EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702.
uint256 constant EIP7702_PREFIX = 0xef0100;

using UserOperationLib for PackedUserOperation;

//get alternate InitCode (just for hashing) when using EIP-7702
function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes32) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function returns a hash, this is not a correct name.

bytes calldata initCode = userOp.initCode;
if (! _isEip7702InitCode(initCode)) {
return 0;
}
address delegate = _getEip7702Delegate(userOp.getSender());
if (initCode.length < 20)
return keccak256(abi.encodePacked(delegate));
else
return keccak256(abi.encodePacked(delegate, initCode[20 :]));
}


function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you write this function without the assembly tricks, how much more gas does it spend? This code is too elaborate for the task of "compare first three bytes".


if (initCode.length < 3) {
return false;
}
uint256 initCodeStart;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
initCodeStart := calldataload(initCode.offset)
}
// make sure first 20 bytes of initCode are "0xff0100" (padded with zeros)
// initCode can be shorter (e.g. only 3), but then it is already zero-padded.
return (initCodeStart >> (256 - 160)) == ((EIP7702_PREFIX << (160 - 24)));
}

/**
* get the EIP-7702 delegate from contract code.
* requires EXTCODECOPY pr: https://github.com/ethereum/EIPs/pull/9248 (not yet merged or implemented)
**/
function _getEip7702Delegate(address sender) view returns (address) {
uint256 senderCode;

// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
extcodecopy(sender, 0, 0, 32)
senderCode := mload(0)
}
Comment on lines +49 to +53
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need any assembly, you can just do sender.code. Also if the code is not exactly 23 bytes long it should revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. using assembly to avoid creating a dynamic bytes array (even with sender.code, we'd need assembly to grab just the address out of it, as there is no "slice" for memory..)
  2. No need to check it is 23 bytes: since eip-3541, no deployed contract can start with "0xEF", unless it is EOF. The "0xEF0100" prefix of eip-7702 is invalid as EOF, such checking the prefix implies the 23 bytes length...

Copy link
Collaborator

@forshtat forshtat Feb 3, 2025

Choose a reason for hiding this comment

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

Even with sender.code, we'd need assembly to grab just the address out of it

I think Solidity now supports just converting bytes to bytes32 to uint256 so as far as I can tell there should be no difference. I may be wrong though.

        bytes memory b = sender.code;
        uint256 senderCode = uint256(bytes32(b));

No need to check it is 23 bytes since EIP-3541

I am concerned some up-and-coming L2s may be a little less rigorous in their implementation of EIP-3541 and that would allow some kind of an attack down the line. It would be dirt-cheap to just double-check.

// senderCode is the first 32 bytes of the sender's code
// If it is an EIP-7702 delegate, then top 24 bits are the EIP7702_PREFIX
// next 160 bytes are the delegate address
require(senderCode >> (256 - 24) == EIP7702_PREFIX, "not an EIP-7702 delegate");
return address(uint160(senderCode >> (256 - 160 - 24)));
}
9 changes: 8 additions & 1 deletion contracts/core/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "./NonceManager.sol";
import "./SenderCreator.sol";
import "./StakeManager.sol";
import "./UserOperationLib.sol";
import "./Eip7702Support.sol";

import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
Expand Down Expand Up @@ -378,8 +379,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
function getUserOpHash(
PackedUserOperation calldata userOp
) public view returns (bytes32) {
bytes32 overrideInitCode = _getEip7702InitCodeOverride(userOp);
return
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash());
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCode));
}

/**
Expand Down Expand Up @@ -441,6 +443,11 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
) internal {
if (initCode.length != 0) {
address sender = opInfo.mUserOp.sender;
if ( _isEip7702InitCode(initCode) ) {
//already validated it is an EIP-7702 delegate (and hence, already has code)
senderCreator().initEip7702Sender(sender, initCode[20:]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was under impression that you wanted to call initEip7702Sender only if initCode contains some data in addition to 0xff0100, but this code seems to call it every time.

return;
}
if (sender.code.length != 0)
revert FailedOp(opIndex, "AA10 sender already constructed");
address sender1 = senderCreator().createSender{
Expand Down
2 changes: 1 addition & 1 deletion contracts/core/EntryPointSimulations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {

//slightly stricter gas limit than the real EntryPoint
function _getVerificationGasLimit(uint256 verificationGasLimit) internal pure virtual override returns (uint256) {
return verificationGasLimit - 300;
return verificationGasLimit - 350;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Magic number. As you need to change it, please extract a constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unrelated. we're going to remove "Simulation" from the critical path, and thus all this code will be removed.

}


Expand Down
26 changes: 19 additions & 7 deletions contracts/core/SenderCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import "../interfaces/ISenderCreator.sol";
import "../utils/Exec.sol";

/**
* Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
Expand All @@ -23,10 +24,9 @@ contract SenderCreator is ISenderCreator {
function createSender(
bytes calldata initCode
) external returns (address sender) {
if (msg.sender != entryPoint) {
revert("AA97 should call from EntryPoint");
}
require(msg.sender == entryPoint, "AA97 should call from EntryPoint");
address factory = address(bytes20(initCode[0:20]));

bytes memory initCallData = initCode[20:];
bool success;
/* solhint-disable no-inline-assembly */
Expand All @@ -40,10 +40,22 @@ contract SenderCreator is ISenderCreator {
0,
32
)
sender := mload(0)
}
if (!success) {
sender = address(0);
if success {
sender := mload(0)
}
}
}

// use initCode to initialize an EIP-7702 account
// caller (EntryPoint) already verified it is an EIP-7702 account.
function initEip7702Sender(
address sender,
bytes calldata initCode
) external {
require(msg.sender == entryPoint, "AA97 should call from EntryPoint");
bytes memory initCallData = initCode[20 :];
// solhint-disable-next-line avoid-low-level-calls
bool success = Exec.call(sender, 0, initCallData, gasleft());
require(success, "AA13 EIP7702 sender init failed");
}
}
20 changes: 17 additions & 3 deletions contracts/core/UserOperationLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,22 @@ library UserOperationLib {
*/
function encode(
PackedUserOperation calldata userOp
) internal pure returns (bytes memory ret) {
return encode(userOp, "");
}

/**
* Pack the user operation data into bytes for hashing.
* @param userOp - The user operation data.
* @param overrideInitCode - If set, encode this instead of the initCode field in the userOp.
*/
function encode(
PackedUserOperation calldata userOp,
bytes32 overrideInitCode
) internal pure returns (bytes memory ret) {
address sender = getSender(userOp);
uint256 nonce = userOp.nonce;
bytes32 hashInitCode = calldataKeccak(userOp.initCode);
bytes32 hashInitCode = overrideInitCode==0 ? calldataKeccak(userOp.initCode) : overrideInitCode;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just make a function _getEip7702InitCodeHash and call it from here, maybe?

bytes32 hashCallData = calldataKeccak(userOp.callData);
bytes32 accountGasLimits = userOp.accountGasLimits;
uint256 preVerificationGas = userOp.preVerificationGas;
Expand Down Expand Up @@ -136,10 +148,12 @@ library UserOperationLib {
/**
* Hash the user operation data.
* @param userOp - The user operation data.
* @param overrideInitCode - If set, the initCode will be replaced with this value just for hashing.
*/
function hash(
PackedUserOperation calldata userOp
PackedUserOperation calldata userOp,
bytes32 overrideInitCode
) internal pure returns (bytes32) {
return keccak256(encode(userOp));
return keccak256(encode(userOp, overrideInitCode));
}
}
3 changes: 3 additions & 0 deletions contracts/interfaces/ISenderCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ interface ISenderCreator {
* @return sender Address of the newly created sender contract.
*/
function createSender(bytes calldata initCode) external returns (address sender);

// call initCode to initialize an EIP-7702 account
function initEip7702Sender(address sender, bytes calldata initCode) external;
}
2 changes: 1 addition & 1 deletion contracts/test/TestUtil.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ contract TestUtil {
using UserOperationLib for PackedUserOperation;

function encodeUserOp(PackedUserOperation calldata op) external pure returns (bytes memory){
return op.encode();
return op.encode(0);
}

}
42 changes: 21 additions & 21 deletions reports/gas-checker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
the destination is "account.entryPoint()", which is known to be "hot" address used by this account
it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target)
╔══════════════════════════╤════════╗
║ gas estimate "simple" │ 29259
║ gas estimate "simple" │ 29247
╟──────────────────────────┼────────╢
║ gas estimate "big tx 5k" │ 114702 ║
╚══════════════════════════╧════════╝
Expand All @@ -12,44 +12,44 @@
║ │ │ │ (delta for │ (compared to ║
║ │ │ │ one UserOp) │ account.exec()) ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 1 │ 77450 │ │ ║
║ simple │ 1 │ 77779 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 2 │ │ 4159812339
║ simple - diff from previous │ 2 │ │ 4195112704
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 10 │ 451980 │ │ ║
║ simple │ 10 │ 455462 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 11 │ │ 4165912400
║ simple - diff from previous │ 11 │ │ 4197612729
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 1 │ 83281 │ │ ║
║ simple paymaster │ 1 │ 83634 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 2 │ │ 4014210883
║ simple paymaster with diff │ 2 │ │ 4048311236
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 10 │ 444666 │ │ ║
║ simple paymaster │ 10 │ 448196 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 11 │ │ 4016310904
║ simple paymaster with diff │ 11 │ │ 4052811281
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 1 │ 167209 │ │ ║
║ big tx 5k │ 1 │ 167526 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 2 │ │ 13079916097
║ big tx - diff from previous │ 2 │ │ 13121216510
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 10 │ 1344654 │ │ ║
║ big tx 5k │ 10 │ 1348136 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 11 │ │ 13087216170
║ big tx - diff from previous │ 11 │ │ 13122516523
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 1 │ 84458 │ │ ║
║ paymaster+postOp │ 1 │ 84811 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 2 │ │ 4130612047
║ paymaster+postOp with diff │ 2 │ │ 4167112424
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 10 │ 456393 │ │ ║
║ paymaster+postOp │ 10 │ 459971 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 11 │ │ 4138112122
║ paymaster+postOp with diff │ 11 │ │ 4173412487
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ token paymaster │ 1 │ 121572 │ │ ║
║ token paymaster │ 1 │ 121925 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ token paymaster with diff │ 2 │ │ 6113031871
║ token paymaster with diff │ 2 │ │ 6148332236
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ token paymaster │ 10 │ 671901 │ │ ║
║ token paymaster │ 10 │ 675467 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ token paymaster with diff │ 11 │ │ 6120031941
║ token paymaster with diff │ 11 │ │ 6156532318
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝

6 changes: 5 additions & 1 deletion src/Create2Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ export class Create2Factory {
to: Create2Factory.factoryDeployer,
value: BigNumber.from(Create2Factory.factoryDeploymentFee)
})
await this.provider.sendTransaction(Create2Factory.factoryTx)
// first tx.. can't "wait" for it.
await new Promise(resolve => setTimeout(resolve, 100))

await this.provider.sendTransaction(Create2Factory.factoryTx).then(async tx => tx.wait())

if (!await this._isFactoryDeployed()) {
throw new Error('fatal: failed to deploy deterministic deployer')
}
Expand Down
Loading
Loading