diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index 89727cff069..c50696e0768 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol"; import {Math} from "../../utils/math/Math.sol"; import {Call} from "../../utils/Call.sol"; +import {Memory} from "../../utils/Memory.sol"; import {Packing} from "../../utils/Packing.sol"; library ERC4337Utils { @@ -198,6 +199,7 @@ library ERC4337Utils { } function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view { + Memory.FreePtr ptr = Memory.save(); self.sender = source.sender; self.nonce = source.nonce; (self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split(); @@ -218,6 +220,7 @@ library ERC4337Utils { self.prefund = 0; self.preOpGas = 0; self.context = ""; + Memory.load(ptr); } function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) { diff --git a/test/abstraction/entrypoint.test.js b/test/abstraction/entrypoint.test.js index 443bbdec49b..278cfd184ef 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -1,30 +1,20 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); + +const { ERC4337Context } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); - const entrypoint = await ethers.deployContract('EntryPoint'); - const factory = await ethers.deployContract('$Create2'); - - const makeAA = (user, salt = ethers.randomBytes(32)) => - ethers.getContractFactory('SimpleAccount').then(accountFactory => - accountFactory - .getDeployTransaction(entrypoint, user) - .then(tx => factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) - .then(deployCode => ethers.concat([factory.target, deployCode])) - .then(initCode => - entrypoint.getSenderAddress - .staticCall(initCode) - .then(sender => Object.assign(accountFactory.attach(sender), { initCode, salt })), - ), - ); + const context = new ERC4337Context(); + await context.wait(); return { accounts, - entrypoint, - factory, - makeAA, + context, + entrypoint: context.entrypoint, + factory: context.factory, }; } @@ -36,37 +26,26 @@ describe('EntryPoint', function () { it('', async function () { const user = this.accounts[0]; const beneficiary = this.accounts[1]; - const sender = await this.makeAA(user); + const sender = await this.context.newAccount(user); expect(await ethers.provider.getCode(sender)).to.equal('0x'); await user.sendTransaction({ to: sender, value: ethers.parseEther('1') }); - await expect( - this.entrypoint.handleOps( - [ - { - sender, - nonce: 0n, - initCode: sender.initCode, - callData: '0x', - accountGasLimits: ethers.toBeHex((2000000n << 128n) | 100000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes) - preVerificationGas: 100000n, - gasFees: ethers.toBeHex((100000n << 128n) | 100000n, 32), // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes) - paymasterAndData: '0x', // concatenation of paymaster fields (or empty) - signature: '0x', - }, - ], - beneficiary, - ), - ) + + const operation = sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], beneficiary)) .to.emit(sender, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, user) .to.emit(this.factory, 'return$deploy') .withArgs(sender) .to.emit(this.entrypoint, 'AccountDeployed') - .to.emit(this.entrypoint, 'Transfer') // Deposit + .withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress) + .to.emit(this.entrypoint, 'Transfer') + .withArgs(ethers.ZeroAddress, sender, anyValue) .to.emit(this.entrypoint, 'BeforeExecution') - .to.emit(this.entrypoint, 'UserOperationEvent'); + // BeforeExecution has no args + .to.emit(this.entrypoint, 'UserOperationEvent') + .withArgs(operation.hash, sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); expect(await ethers.provider.getCode(sender)).to.not.equal('0x'); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..0375b271982 --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,112 @@ +const { ethers } = require('hardhat'); + +function pack(left, right) { + return ethers.toBeHex((left << 128n) | right, 32); +} + +class ERC4337Context { + constructor() { + this.entrypointAsPromise = ethers.deployContract('EntryPoint'); + this.factoryAsPromise = ethers.deployContract('$Create2'); + this.accountAsPromise = ethers.getContractFactory('SimpleAccount'); + this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId); + } + + async wait() { + this.entrypoint = await this.entrypointAsPromise; + this.factory = await this.factoryAsPromise; + this.account = await this.accountAsPromise; + this.chainId = await this.chainIdAsPromise; + return this; + } + + async newAccount(user, salt = ethers.randomBytes(32)) { + await this.wait(); + const initCode = await this.account + .getDeployTransaction(this.entrypoint, user) + .then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data])) + .then(deployCode => ethers.concat([this.factory.target, deployCode])); + const instance = await this.entrypoint.getSenderAddress + .staticCall(initCode) + .then(address => this.account.attach(address)); + return new AbstractAccount(instance, initCode, this); + } +} + +class AbstractAccount extends ethers.BaseContract { + constructor(instance, initCode, context) { + super(instance.target, instance.interface, instance.runner, instance.deployTx); + this.address = instance.target; + this.initCode = initCode; + this.context = context; + } + + createOp(params = {}, withInit = false) { + return new UserOperation({ + ...params, + sender: this, + initCode: withInit ? this.initCode : '0x', + }); + } +} + +class UserOperation { + constructor(params) { + this.sender = params.sender; + this.nonce = params.nonce ?? 0n; + this.initCode = params.initCode ?? '0x'; + this.callData = params.callData ?? '0x'; + this.verificationGas = params.verificationGas ?? 2_000_000n; + this.callGas = params.callGas ?? 100_000n; + this.preVerificationGas = params.preVerificationGas ?? 100_000n; + this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; + this.maxFeePerGas = params.maxFeePerGas ?? 100_000n; + this.paymasterAndData = params.paymasterAndData ?? '0x'; + this.signature = params.signature ?? '0x'; + } + + get packed() { + return { + sender: this.sender, + nonce: this.nonce, + initCode: this.initCode, + callData: this.callData, + accountGasLimits: pack(this.verificationGas, this.callGas), + preVerificationGas: this.preVerificationGas, + gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), + paymasterAndData: this.paymasterAndData, + signature: this.signature, + }; + } + + get hash() { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender.target, + p.nonce, + ethers.keccak256(p.initCode), + ethers.keccak256(p.callData), + p.accountGasLimits, + p.preVerificationGas, + p.gasFees, + ethers.keccak256(p.paymasterAndData), + ], + ), + ); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes32', 'address', 'uint256'], + [h, this.sender.context.entrypoint.target, this.sender.context.chainId], + ), + ); + } +} + +module.exports = { + ERC4337Context, + AbstractAccount, + UserOperation, +};