-
Notifications
You must be signed in to change notification settings - Fork 820
AA-521 EntryPoint support for eip-7702 #529
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
Changes from all commits
db002af
edb2255
dc43285
fc0c4e7
263a232
5b052ae
38d4714
5f0a6b7
68894e4
317d26b
2617f03
5052317
b8a9d26
d9b3f77
24b5473
fafeca8
f98cd95
fb06bc3
69499d0
bac3113
e54c2a7
146dbbe
0860e41
f25a26b
ee54899
33b294a
ea09602
8dafd94
c026b2e
1442357
b088f05
6459a16
192be39
26d6a0b
a747be1
f7c1688
074844c
fac94f7
de69b3b
d4a2802
539f7b7
6fcc930
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| }; |
| 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)); | ||
| } | ||
| } |
| 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, | ||
|
|
@@ -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, | ||
|
|
@@ -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(), | ||
|
|
@@ -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( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.