Skip to content

Commit

Permalink
tests passes, including 7702-enabled external geth
Browse files Browse the repository at this point in the history
  • Loading branch information
drortirosh committed Jan 29, 2025
1 parent f98cd95 commit 951ab2a
Show file tree
Hide file tree
Showing 11 changed files with 553 additions and 65 deletions.
54 changes: 47 additions & 7 deletions contracts/core/Eip7702Support.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import "../core/UserOperationLib.sol";
// 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;
using UserOperationLib for PackedUserOperation;

//get alternate InitCode (just for hashing) when using EIP-7702
function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes32) {
//get alternate InitCodeHash (just for UserOp hash) when using EIP-7702
function _getEip7702InitCodeHashOverride(PackedUserOperation calldata userOp) view returns (bytes32) {
bytes calldata initCode = userOp.initCode;
if (! _isEip7702InitCode(initCode)) {
if (!_isEip7702InitCode(initCode)) {
return 0;
}
address delegate = _getEip7702Delegate(userOp.getSender());
if (initCode.length < 20)
if (initCode.length <= 20)
return keccak256(abi.encodePacked(delegate));
else
return keccak256(abi.encodePacked(delegate, initCode[20 :]));
Expand All @@ -25,7 +25,7 @@ using UserOperationLib for PackedUserOperation;

function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) {

if (initCode.length < 3) {
if (initCode.length < 2) {
return false;
}
uint256 initCodeStart;
Expand All @@ -43,6 +43,7 @@ using UserOperationLib for PackedUserOperation;
* 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
Expand All @@ -53,6 +54,45 @@ using UserOperationLib for PackedUserOperation;
// 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");
if (senderCode >> (256 - 24) != EIP7702_PREFIX) {
// instead of just "not an EIP-7702 delegate", if some info.
require(sender.code.length > 0, "sender has no code");
require(sender.code.length == 23, string.concat("sender code length is not 23: ",
toString(sender.code.length)," ", toHex(senderCode)));
revert("not an EIP-7702 delegate");
}
return address(uint160(senderCode >> (256 - 160 - 24)));
}

function toHexNibble(uint8 b) pure returns (bytes1) {
if (b < 10) {
return bytes1(uint8(b) + 0x30);
} else {
return bytes1(uint8(b) + 0x57);
}
}


function toHex(bytes1 b) pure returns (string memory) {
bytes memory s = new bytes(2);
s[0] = toHexNibble(uint8(b) / 16);
s[1] = toHexNibble(uint8(b) % 16);
return string(s);
}

function toHex(uint x) pure returns (string memory ret) {
ret = "";
while (x != 0) {
bytes1 b = bytes1(uint8(x % 256));
x = x / 256;
ret = string.concat(toHex(b), ret);
}
ret = string.concat("0x", ret);
}

function toString(uint n) pure returns (string memory s) {
bytes1 s0 = bytes1(uint8(48 + n / 100));
bytes1 s1 = bytes1(uint8(48 + n / 10 % 10));
bytes1 s2 = bytes1(uint8(48 + n % 10));
return string(abi.encodePacked(s0, s1, s2));
}
10 changes: 6 additions & 4 deletions contracts/core/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
function getUserOpHash(
PackedUserOperation calldata userOp
) public view returns (bytes32) {
bytes32 overrideInitCode = _getEip7702InitCodeOverride(userOp);
bytes32 overrideInitCodeHash = _getEip7702InitCodeHashOverride(userOp);
return
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCode));
MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash));
}

/**
Expand Down Expand Up @@ -444,8 +444,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
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:]);
if (initCode.length>20 ) {
//already validated it is an EIP-7702 delegate (and hence, already has code)
senderCreator().initEip7702Sender(sender, initCode[20:]);
}
return;
}
if (sender.code.length != 0)
Expand Down
9 changes: 6 additions & 3 deletions contracts/core/SenderCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.23;

import "../interfaces/ISenderCreator.sol";
import "../utils/Exec.sol";
import {IEntryPoint} from "../interfaces/IEntryPoint.sol";

/**
* Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
Expand Down Expand Up @@ -50,12 +51,14 @@ contract SenderCreator is ISenderCreator {
// caller (EntryPoint) already verified it is an EIP-7702 account.
function initEip7702Sender(
address sender,
bytes calldata initCode
bytes calldata initCallData
) 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");
if (!success) {
bytes memory result = Exec.getReturnData(2048);
revert IEntryPoint.FailedOpWithRevert(0,"AA13 EIP7702 sender init failed", result);
}
}
}
12 changes: 6 additions & 6 deletions contracts/core/UserOperationLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ library UserOperationLib {
/**
* 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.
* @param overrideInitCodeHash - If set, encode this instead of the initCode field in the userOp.
*/
function encode(
PackedUserOperation calldata userOp,
bytes32 overrideInitCode
bytes32 overrideInitCodeHash
) internal pure returns (bytes memory ret) {
address sender = getSender(userOp);
uint256 nonce = userOp.nonce;
bytes32 hashInitCode = overrideInitCode==0 ? calldataKeccak(userOp.initCode) : overrideInitCode;
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 @@ -148,12 +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.
* @param overrideInitCodeHash - If set, the initCode hash will be replaced with this value just for UserOp hashing.
*/
function hash(
PackedUserOperation calldata userOp,
bytes32 overrideInitCode
bytes32 overrideInitCodeHash
) internal pure returns (bytes32) {
return keccak256(encode(userOp, overrideInitCode));
return keccak256(encode(userOp, overrideInitCodeHash));
}
}
61 changes: 61 additions & 0 deletions contracts/test/TestEip7702DelegateAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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;
}


function selfcode() internal returns (uint ret) {
assembly {
extcodecopy(address(), 0, 0, 32)
ret := mload(0)
}
}
}
5 changes: 4 additions & 1 deletion contracts/test/TestUtil.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.23;

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

contract TestUtil {
using UserOperationLib for PackedUserOperation;
Expand All @@ -11,4 +11,7 @@ contract TestUtil {
return op.encode(0);
}

function _isEip7702InitCode(bytes calldata initCode) external pure returns (bool) {
return _isEip7702InitCode(initCode);
}
}
5 changes: 3 additions & 2 deletions src/Create2Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export class Create2Factory {
gasLimit = Math.floor(gasLimit * 64 / 63)
}

const ret = await this.signer.sendTransaction({ ...deployTx, gasLimit })
await ret.wait()
await this.signer.sendTransaction({ ...deployTx, gasLimit }).then(async tx => tx.wait())

if (await this.provider.getCode(addr).then(code => code.length) === 2) {
throw new Error('failed to deploy')
}
Expand Down Expand Up @@ -98,6 +98,7 @@ export class Create2Factory {
if (await this._isFactoryDeployed()) {
return
}

await (signer ?? this.signer).sendTransaction({
to: Create2Factory.factoryDeployer,
value: BigNumber.from(Create2Factory.factoryDeploymentFee)
Expand Down
77 changes: 77 additions & 0 deletions test/GethExecutable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { spawn, ChildProcess } from 'child_process'
import Debug from 'debug'

const debug = Debug('aa.geth')

const port = 54321
export const gethLauncher = {
name: 'geth',
exec: './scripts/geth.sh',
args: `--http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.port=${port}`
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const anvilLauncher = {
name: 'anvil',
exec: './scripts/anvil.sh',
args: `--hardfork prague --port=${port}`
}

export class GethExecutable {
constructor (private readonly impl = gethLauncher) {
}

private gethProcess: ChildProcess | null = null

markerString = /HTTP server started|Listening on/

rpcUrl (): string {
return `http://localhost:${port}`
}

async init (): Promise<void> {
return new Promise((resolve, reject) => {
console.log('spawning: ', this.impl.exec, this.impl.args)
this.gethProcess = spawn(this.impl.exec, this.impl.args.split(' '))

let allData = ''
if (this.gethProcess != null) {
const timeout = setTimeout(() => {
reject(new Error(`Timed out waiting for marker regex: ${this.markerString.toString()}\n: ${allData}`))
}, 5000)

this.gethProcess.stdout?.on('data', (data: string) => {
data = data.toString()
allData += data
debug('stdout:', data)
if (data.match(this.markerString) != null) {
clearTimeout(timeout)
resolve()
}
})
this.gethProcess.stderr?.on('data', (data: string) => {
data = data.toString()
allData += data
debug('stderr:', data)

if (data.match(this.markerString) != null) {
clearTimeout(timeout)
resolve()
}
})

this.gethProcess.on('exit', (code: number | null) => {
console.log(`${this.impl.name} process exited with code ${code}`)
})
} else {
reject(new Error('Failed to start geth process'))
}
})
}

done (): void {
if (this.gethProcess != null) {
this.gethProcess.kill()
}
}
}
Loading

0 comments on commit 951ab2a

Please sign in to comment.