Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
a747be1
gascalc
drortirosh Feb 11, 2025
f7c1688
remove Simple7702Account, into a separate PR
drortirosh Feb 11, 2025
074844c
PR review: cleanup Eip7702Support assembly usage.
drortirosh Feb 12, 2025
fac94f7
update comments
drortirosh Feb 13, 2025
de69b3b
refactor library. split constants.
drortirosh Feb 16, 2025
d4a2802
fix comment
drortirosh Feb 16, 2025
539f7b7
merge
drortirosh Feb 16, 2025
6fcc930
Merge branch 'develop' into AA-521-ep-7702
drortirosh Feb 16, 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
4 changes: 2 additions & 2 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module.exports = {
skipFiles: [
"test",
"samples/bls/lib",
"utils/Exec.sol"
"utils/Exec.sol",
"samples"
],
configureYulOptimizer: true,
};
67 changes: 67 additions & 0 deletions contracts/core/Eip7702Support.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
pragma solidity ^0.8;
// SPDX-License-Identifier: MIT
// solhint-disable no-inline-assembly

import "../interfaces/PackedUserOperation.sol";
import "../core/UserOperationLib.sol";

library Eip7702Support {

// EIP-7702 code prefix before delegate address.
bytes3 internal constant EIP7702_PREFIX = 0xef0100;

// EIP-7702 initCode marker, to specify this account is EIP-7702.
bytes2 internal constant INITCODE_EIP7702_MARKER = 0x7702;

using UserOperationLib for PackedUserOperation;

//get alternate InitCodeHash (just for UserOp hash) when using EIP-7702
function _getEip7702InitCodeHashOverride(PackedUserOperation calldata userOp) internal view returns (bytes32) {
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 :]));
}

// check if this initCode is EIP-7702: starts with INITCODE_EIP7702_MARKER.
function _isEip7702InitCode(bytes calldata initCode) internal pure returns (bool) {

if (initCode.length < 2) {
return false;
}
bytes20 initCodeStart;
// non-empty calldata bytes are always zero-padded to 32-bytes, so can be safely casted to "bytes20"
assembly ("memory-safe") {
initCodeStart := calldataload(initCode.offset)
}
// make sure first 20 bytes of initCode are "0x7702" (padded with zeros)
return initCodeStart == bytes20(INITCODE_EIP7702_MARKER);
}

/**
* get the EIP-7702 delegate from contract code.
* must only be used if _isEip7702InitCode(initCode) is true.
*/
function _getEip7702Delegate(address sender) internal view returns (address) {

bytes32 senderCode;

assembly ("memory-safe") {
extcodecopy(sender, 0, 0, 23)
senderCode := mload(0)
}
// To be a valid EIP-7702 delegate, the first 3 bytes are EIP7702_PREFIX
// followed by the delegate address
if (bytes3(senderCode) != EIP7702_PREFIX) {
// instead of just "not an EIP-7702 delegate", if some info.
require(sender.code.length > 0, "sender has no code");
revert("not an EIP-7702 delegate");
}
return address(bytes20(senderCode << 24));
}
}
11 changes: 10 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 @@ -381,8 +382,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
function getUserOpHash(
PackedUserOperation calldata userOp
) public view returns (bytes32) {
bytes32 overrideInitCodeHash = Eip7702Support._getEip7702InitCodeHashOverride(userOp);
return
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash());
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash));
}

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

Choose a reason for hiding this comment

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

This means that EIP-7702 accounts can get two validation phase calls instead of one whenever they want, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes. eip-7702 account is never "new" (from the EntryPoint's view) - it is always "already created".
So we always allow an eip-7702 account to run "initialization" code - as many times as it wishes.
The alternative was to require it to put this initialization data into the "callCode", use it from "validate" and then ignore it during execution.

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

function supportsInterface(bytes4) public view virtual override returns (bool) {
return false;
}
}
33 changes: 25 additions & 8 deletions contracts/core/SenderCreator.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */

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

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

uint256 private constant REVERT_REASON_MAX_LEN = 2048;

/**
* Call the "initCode" factory to create and return the sender account address.
* @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address,
Expand All @@ -23,13 +29,11 @@ contract SenderCreator is ISenderCreator {
function createSender(
bytes calldata initCode
) external returns (address sender) {
if (msg.sender != entryPoint) {
revert("AA97 should call from EntryPoint");
}
address factory = address(bytes20(initCode[0:20]));
bytes memory initCallData = initCode[20:];
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 */
assembly ("memory-safe") {
success := call(
gas(),
Expand All @@ -40,10 +44,23 @@ contract SenderCreator is ISenderCreator {
0,
32
)
sender := mload(0)
if success {
sender := mload(0)
}
}
}

// use initCallData to initialize an EIP-7702 account
// caller (EntryPoint) already verified it is an EIP-7702 account.
function initEip7702Sender(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why can't we use createSender again? It is doing basically the same thing, calling address(initCode[:20] with initCode[20:], what are the main differences?
If they are not critical, let's merge these two methods?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. createSender is used to physically create, not initialize.
  2. it would require changing createSender signature, since it requires an extra parameter (not extracting it from initCode)
  3. we already suggested to use call({from:entryPoint, to:senderCreator, data: encode(createSender(...) } as a view function to get account address. changing the API would break it.
  4. so seemed like adding a new one was a better choice.

address sender,
bytes calldata initCallData
) external {
require(msg.sender == entryPoint, "AA97 should call from EntryPoint");
bool success = Exec.call(sender, 0, initCallData, gasleft());
if (!success) {
sender = address(0);
bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN);
revert IEntryPoint.FailedOpWithRevert(0, "AA13 EIP7702 sender init failed", result);
}
}
}
12 changes: 8 additions & 4 deletions contracts/core/UserOperationLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ library UserOperationLib {
/**
* Pack the user operation data into bytes for hashing.
* @param userOp - The user operation data.
* @param overrideInitCodeHash - If set, encode this instead of the initCode field in the userOp.
*/
function encode(
PackedUserOperation calldata userOp
PackedUserOperation calldata userOp,
bytes32 overrideInitCodeHash
) internal pure returns (bytes memory ret) {
address sender = getSender(userOp);
uint256 nonce = userOp.nonce;
bytes32 hashInitCode = calldataKeccak(userOp.initCode);
bytes32 hashInitCode = overrideInitCodeHash != 0 ? overrideInitCodeHash : calldataKeccak(userOp.initCode);
bytes32 hashCallData = calldataKeccak(userOp.callData);
bytes32 accountGasLimits = userOp.accountGasLimits;
uint256 preVerificationGas = userOp.preVerificationGas;
Expand Down Expand Up @@ -136,10 +138,12 @@ library UserOperationLib {
/**
* Hash the user operation data.
* @param userOp - The user operation data.
* @param overrideInitCodeHash - If set, the initCode hash will be replaced with this value just for UserOp hashing.
*/
function hash(
PackedUserOperation calldata userOp
PackedUserOperation calldata userOp,
bytes32 overrideInitCodeHash
) internal pure returns (bytes32) {
return keccak256(encode(userOp));
return keccak256(encode(userOp, overrideInitCodeHash));
}
}
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;
}
53 changes: 53 additions & 0 deletions contracts/test/TestEip7702DelegateAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
pragma solidity ^0.8.23;
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../core/BaseAccount.sol";
import "../core/Eip7702Support.sol";

contract TestEip7702DelegateAccount is BaseAccount {

IEntryPoint private immutable _entryPoint;
bool public testInitCalled;

constructor(IEntryPoint anEntryPoint) {
_entryPoint = anEntryPoint;
}

function testInit() public {
testInitCalled = true;
}

function entryPoint() public view override virtual returns (IEntryPoint) {
return _entryPoint;
}

// Require the function call went through EntryPoint or owner
function _requireFromEntryPointOrOwner() internal view {
require(msg.sender == address(this) || msg.sender == address(entryPoint()), "account: not Owner or EntryPoint");
}

/**
* execute a transaction (called directly from owner, or by entryPoint)
* @param dest destination address to call
* @param value the value to pass in this call
* @param func the calldata to pass in this call
*/
function execute(address dest, uint256 value, bytes calldata func) external {
_requireFromEntryPointOrOwner();
(bool success,) = dest.call{value: value}(func);
require(success, "call failed");
}

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual override returns (uint256 validationData) {
if (userOp.initCode.length > 20) {
require(testInitCalled, "testInit not called");
}
if (ECDSA.recover(userOpHash, userOp.signature) == address(this)) {
return 0;
}
return 1;
}
}
7 changes: 5 additions & 2 deletions contracts/test/TestUtil.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
pragma solidity ^0.8.23;

import "../interfaces/PackedUserOperation.sol";
import "../core/UserOperationLib.sol";
import "../core/Eip7702Support.sol";

contract TestUtil {
using UserOperationLib for PackedUserOperation;

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

function isEip7702InitCode(bytes calldata initCode) external pure returns (bool) {
return Eip7702Support._isEip7702InitCode(initCode);
}
}
32 changes: 16 additions & 16 deletions reports/gas-checker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,36 @@
║ │ │ │ (delta for │ (compared to ║
║ │ │ │ one UserOp) │ account.exec()) ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 1 │ 77468 │ │ ║
║ simple │ 1 │ 77824 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 2 │ │ 4160412345
║ simple - diff from previous │ 2 │ │ 4199612737
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 10 │ 452160 │ │ ║
║ simple │ 10 │ 455792 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 11 │ │ 4167712418
║ simple - diff from previous │ 11 │ │ 4203312774
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 1 │ 83307 │ │ ║
║ simple paymaster │ 1 │ 83675 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 2 │ │ 4015610897
║ simple paymaster with diff │ 2 │ │ 4052411265
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 10 │ 444914 │ │ ║
║ simple paymaster │ 10 │ 448582 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 11 │ │ 4017710918
║ simple paymaster with diff │ 11 │ │ 4059311334
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 1 │ 167233 │ │ ║
║ big tx 5k │ 1 │ 167613 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 2 │ │ 13087116169
║ big tx - diff from previous │ 2 │ │ 13121516513
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 10 │ 1345038 │ │ ║
║ big tx 5k │ 10 │ 1348706 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 11 │ │ 13089616194
║ big tx - diff from previous │ 11 │ │ 13122816526
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 1 │ 84690 │ │ ║
║ paymaster+postOp │ 1 │ 85070 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 2 │ │ 4157412315
║ paymaster+postOp with diff │ 2 │ │ 4193012671
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 10 │ 458869 │ │ ║
║ paymaster+postOp │ 10 │ 462525 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 11 │ │ 4157712318
║ paymaster+postOp with diff │ 11 │ │ 4199312734
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝

3 changes: 2 additions & 1 deletion scripts/docker-gascalc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ services:
localgeth:
ports: [ '8545:8545' ]
image: ethereum/client-go:release-1.14
# image: dtr22/geth7702
command: |
--verbosity 2
--verbosity 1
--http -http.addr 0.0.0.0 --http.api 'eth,net,web3,debug' --http.port 8545
--http.vhosts '*,localhost,host.docker.internal'
--dev --rpc.allow-unprotected-txs
Loading