diff --git a/modules/4337/contracts/test/TestEntryPoint.sol b/modules/4337/contracts/test/TestEntryPoint.sol deleted file mode 100644 index d2ae16d4..00000000 --- a/modules/4337/contracts/test/TestEntryPoint.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -/* solhint-disable one-contract-per-file */ -pragma solidity >=0.8.0; - -import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; -import {INonceManager} from "@account-abstraction/contracts/interfaces/INonceManager.sol"; -import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; - -/** - * helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, - * which is explicitly not the entry point itself. - */ -contract SenderCreator { - /** - * 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, followed by calldata - * @return sender the returned address of the created account, or zero address on failure. - */ - function createSender(bytes calldata initCode) external returns (address sender) { - address factory = address(bytes20(initCode[0:20])); - bytes memory initCallData = initCode[20:]; - bool success; - // solhint-disable-next-line no-inline-assembly - assembly ("memory-safe") { - success := call(gas(), factory, 0, add(initCallData, 0x20), mload(initCallData), 0, 32) - sender := mload(0) - } - if (!success) { - sender = address(0); - } - } -} - -contract TestEntryPoint is INonceManager { - error NotEnoughFunds(uint256 expected, uint256 available); - error InvalidNonce(uint256 userNonce); - event UserOpReverted(bytes reason); - SenderCreator public immutable SENDER_CREATOR; - mapping(address => uint256) public balances; - mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber; - - constructor() { - SENDER_CREATOR = new SenderCreator(); - } - - receive() external payable { - balances[msg.sender] = balances[msg.sender] + msg.value; - } - - function executeUserOp(UserOperation calldata userOp, uint256 requiredPrefund) external { - if (userOp.sender.code.length == 0) { - require(userOp.initCode.length >= 20, "Invalid initCode provided"); - require(userOp.sender == SENDER_CREATOR.createSender(userOp.initCode), "Could not create expected account"); - } - - require(gasleft() > userOp.verificationGasLimit, "Not enough gas for verification"); - - uint256 userBalance = balances[userOp.sender]; - uint256 missingAccountFunds = requiredPrefund > userBalance ? requiredPrefund - userBalance : 0; - - uint256 validationData = IAccount(userOp.sender).validateUserOp{gas: userOp.verificationGasLimit}( - userOp, - bytes32(0), - missingAccountFunds - ); - require(validationData == 0, "Signature validation failed"); - - userBalance = balances[userOp.sender]; - if (userBalance < requiredPrefund) { - revert NotEnoughFunds(requiredPrefund, userBalance); - } - balances[userOp.sender] = userBalance - requiredPrefund; - - if (!_validateAndUpdateNonce(userOp.sender, userOp.nonce)) { - revert InvalidNonce(userOp.nonce); - } - - require(gasleft() > userOp.callGasLimit, "Not enough gas for execution"); - (bool success, bytes memory returnData) = userOp.sender.call{gas: userOp.callGasLimit}(userOp.callData); - if (!success) { - emit UserOpReverted(returnData); - } - } - - function getNonce(address sender, uint192 key) external view override returns (uint256 nonce) { - return nonceSequenceNumber[sender][key] | (uint256(key) << 64); - } - - function incrementNonce(uint192 key) external override { - nonceSequenceNumber[msg.sender][key]++; - } - - function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) { - uint192 key = uint192(nonce >> 64); - uint64 seq = uint64(nonce); - return nonceSequenceNumber[sender][key]++ == seq; - } -} diff --git a/modules/4337/src/deploy/entrypoint.ts b/modules/4337/src/deploy/entrypoint.ts index dc3411c3..29df0e67 100644 --- a/modules/4337/src/deploy/entrypoint.ts +++ b/modules/4337/src/deploy/entrypoint.ts @@ -7,15 +7,7 @@ const deploy: DeployFunction = async ({ deployments, getNamedAccounts, network } const { deployer } = await getNamedAccounts() const { deploy } = deployments - if (network.tags.test) { - await deploy('EntryPoint', { - from: deployer, - contract: 'TestEntryPoint', - args: [], - log: true, - deterministicDeployment: true, - }) - } else if (network.tags.dev) { + if (network.tags.dev || network.tags.test) { await deploy('EntryPoint', { from: deployer, contract: EntryPoint, diff --git a/modules/4337/src/utils/userOp.ts b/modules/4337/src/utils/userOp.ts index 016e7459..16047744 100644 --- a/modules/4337/src/utils/userOp.ts +++ b/modules/4337/src/utils/userOp.ts @@ -80,19 +80,19 @@ export const buildSafeUserOp = (template: OptionalExceptFor { const setupTests = deployments.createFixture(async ({ deployments }) => { await deployments.fixture() - const [user1] = await ethers.getSigners() - const entryPoint = await getEntryPoint() + const [user1, relayer] = await ethers.getSigners() + let entryPoint = await getEntryPoint() + entryPoint = entryPoint.connect(relayer) const module = await getSafe4337Module() const safe = await getTestSafe(user1, await module.getAddress(), await module.getAddress()) return { user1, safe, + relayer, validator: module, entryPoint, } }) - describe('executeUserOp - existing account', () => { + describe('handleOps - existing account', () => { it('should revert with invalid signature', async () => { const { user1, safe, entryPoint } = await setupTests() await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), user1.address, @@ -38,15 +39,19 @@ describe('Safe4337Module - Existing Safe', () => { ) const signature = buildSignatureBytes([await signHash(user1, ethers.keccak256('0xbaddad42'))]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await expect(entryPoint.executeUserOp(userOp, 0)).to.be.revertedWith('Signature validation failed') + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.be.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA24 signature error') + expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) }) - it('should execute contract calls without fee', async () => { - const { user1, safe, validator, entryPoint } = await setupTests() + it('should execute contract calls without a prefund required', async () => { + const { user1, safe, validator, entryPoint, relayer } = await setupTests() - await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) + await entryPoint.depositTo(await safe.getAddress(), { value: ethers.parseEther('1.0') }) + + await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('0.5') }) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), user1.address, @@ -54,22 +59,25 @@ describe('Safe4337Module - Existing Safe', () => { '0x', '0', await entryPoint.getAddress(), + false, + false, ) const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await logGas('Execute UserOp without fee payment', entryPoint.executeUserOp(userOp, 0)) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.5')) + await logGas('Execute UserOp without a prefund payment', entryPoint.handleOps([userOp], relayer)) + expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0')) }) it('should not be able to execute contract calls twice', async () => { const { user1, safe, validator, entryPoint } = await setupTests() + const receiver = ethers.Wallet.createRandom().address await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), - user1.address, + receiver, ethers.parseEther('0.5'), '0x', '0', @@ -78,19 +86,24 @@ describe('Safe4337Module - Existing Safe', () => { const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await entryPoint.executeUserOp(userOp, 0) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.5')) - await expect(entryPoint.executeUserOp(userOp, 0)).to.be.revertedWithCustomError(entryPoint, 'InvalidNonce').withArgs(0) + await entryPoint.handleOps([userOp], user1.address) + expect(await ethers.provider.getBalance(receiver)).to.be.eq(ethers.parseEther('0.5')) + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.be.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce') }) it('should execute contract calls with fee', async () => { const { user1, safe, validator, entryPoint } = await setupTests() + const feeBeneficiary = ethers.Wallet.createRandom().address + const randomAddress = ethers.Wallet.createRandom().address + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.eq(0) await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), - user1.address, + randomAddress, ethers.parseEther('0.5'), '0x', '0', @@ -99,15 +112,19 @@ describe('Safe4337Module - Existing Safe', () => { const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await logGas('Execute UserOp with fee payment', entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001'))) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.499999')) + await logGas('Execute UserOp with fee payment', entryPoint.handleOps([userOp], feeBeneficiary)) + + // checking that the fee was paid + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.gt(0) + // check that the call was executed + expect(await ethers.provider.getBalance(randomAddress)).to.be.eq(ethers.parseEther('0.5')) }) it('reverts on failure', async () => { const { user1, safe, validator, entryPoint } = await setupTests() - await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('0.000001') }) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.000001')) + // Make sure to send enough ETH for the pre-fund but less than the transaction + await entryPoint.depositTo(await safe.getAddress(), { value: ethers.parseEther('1.0') }) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), user1.address, @@ -119,16 +136,16 @@ describe('Safe4337Module - Existing Safe', () => { const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) + const userOpHash = await entryPoint.getUserOpHash(userOp) + const expectedReturnData = validator.interface.encodeErrorResult('Error(string)', ['Execution failed']) - const transaction = await entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001')).then((tx) => tx.wait()) - const logs = transaction.logs.map((log) => entryPoint.interface.parseLog(log)) - const emittedRevert = logs.some((l) => l?.name === 'UserOpReverted') - - expect(emittedRevert).to.be.true + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.emit(entryPoint, 'UserOperationRevertReason') + .withArgs(userOpHash, userOp.sender, 0, expectedReturnData) }) it('executeUserOpWithErrorString should execute contract calls', async () => { - const { user1, safe, validator, entryPoint } = await setupTests() + const { user1, safe, validator, entryPoint, relayer } = await setupTests() await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) @@ -141,11 +158,14 @@ describe('Safe4337Module - Existing Safe', () => { await entryPoint.getAddress(), false, true, + { + maxFeePerGas: '0', + }, ) const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await logGas('Execute UserOp without fee payment and bubble up error string', entryPoint.executeUserOp(userOp, 0)) + await logGas('Execute UserOp without fee payment and bubble up error string', entryPoint.handleOps([userOp], relayer)) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.5')) }) @@ -154,8 +174,8 @@ describe('Safe4337Module - Existing Safe', () => { const reverterContract = await ethers.getContractFactory('TestReverter').then((factory) => factory.deploy()) const callData = reverterContract.interface.encodeFunctionData('alwaysReverting') - await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('0.000001') }) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.000001')) + await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('0.5') }) + const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), await reverterContract.getAddress(), @@ -170,12 +190,11 @@ describe('Safe4337Module - Existing Safe', () => { const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - const transaction = await entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001')).then((tx) => tx.wait()) - const logs = transaction.logs.map((log) => entryPoint.interface.parseLog(log)) ?? [] - const emittedRevert = logs.find((l) => l?.name === 'UserOpReverted') - expect(emittedRevert?.args.reason).to.equal( - reverterContract.interface.encodeErrorResult('Error', ['You called a function that always reverts']), - ) + const expectedRevertReason = validator.interface.encodeErrorResult('Error(string)', ['You called a function that always reverts']) + + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.emit(entryPoint, 'UserOperationRevertReason') + .withArgs(await entryPoint.getUserOpHash(userOp), safeOp.safe, 0, expectedRevertReason) }) }) }) diff --git a/modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts b/modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts index 10393977..da10d122 100644 --- a/modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts +++ b/modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts @@ -10,7 +10,7 @@ describe('Safe4337Module - Newly deployed safe', () => { const setupTests = deployments.createFixture(async ({ deployments }) => { await deployments.fixture() - const [user1] = await ethers.getSigners() + const [user1, relayer] = await ethers.getSigners() const entryPoint = await getEntryPoint() const module = await getSafe4337Module() const proxyFactory = await getFactory() @@ -34,6 +34,7 @@ describe('Safe4337Module - Newly deployed safe', () => { safeModuleSetup, validator: module, entryPoint, + relayer, } }) @@ -61,15 +62,19 @@ describe('Safe4337Module - Newly deployed safe', () => { safeOp, signature, }) - await expect(entryPoint.executeUserOp(userOp, 0)).to.be.revertedWith('Signature validation failed') + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.be.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA24 signature error') + expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('1.0')) }) - it('should execute contract calls without fee', async () => { + it('should execute contract calls without a prefund required', async () => { const { user1, safe, validator, entryPoint } = await setupTests() - await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('1.0')) + await entryPoint.depositTo(await safe.address, { value: ethers.parseEther('1.0') }) + + await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('0.5') }) const safeOp = buildSafeUserOpTransaction( safe.address, user1.address, @@ -83,13 +88,14 @@ describe('Safe4337Module - Newly deployed safe', () => { initCode: safe.getInitCode(), }, ) + const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, }) - await logGas('Execute UserOp without fee payment', entryPoint.executeUserOp(userOp, 0)) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.5')) + await logGas('Execute UserOp without fee payment', entryPoint.handleOps([userOp], user1.address)) + expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0')) }) it('should not be able to execute contract calls twice', async () => { @@ -97,6 +103,8 @@ describe('Safe4337Module - Newly deployed safe', () => { await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('1.0')) + await safe.deploy(user1) + const safeOp = buildSafeUserOpTransaction( safe.address, user1.address, @@ -104,27 +112,23 @@ describe('Safe4337Module - Newly deployed safe', () => { '0x', '0', await entryPoint.getAddress(), - false, - false, - { - initCode: safe.getInitCode(), - }, ) const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, }) - await entryPoint.executeUserOp(userOp, 0) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.5')) - await expect(entryPoint.executeUserOp(userOp, 0)).to.be.revertedWithCustomError(entryPoint, 'InvalidNonce').withArgs(0) + await entryPoint.handleOps([userOp], user1.address) + + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.be.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce') }) it('reverts on failure', async () => { const { user1, safe, validator, entryPoint } = await setupTests() - await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('0.000001') }) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.000001')) + await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('0.05') }) const safeOp = buildSafeUserOpTransaction( safe.address, user1.address, @@ -143,22 +147,23 @@ describe('Safe4337Module - Newly deployed safe', () => { safeOp, signature, }) - const transaction = await logGas('Execute UserOp without fee payment', entryPoint.executeUserOp(userOp, 0)) - const receipt = await transaction.wait() - const logs = receipt.logs.map((log) => entryPoint.interface.parseLog(log)) - const emittedRevert = logs.some((log) => log?.name === 'UserOpReverted') - expect(emittedRevert).to.be.true - expect(await safe.isDeployed()).to.be.true + const expectedReturnData = validator.interface.encodeErrorResult('Error(string)', ['Execution failed']) + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.emit(entryPoint, 'UserOperationRevertReason') + .withArgs(await entryPoint.getUserOpHash(userOp), userOp.sender, 0, expectedReturnData) }) it('should execute contract calls with fee', async () => { const { user1, safe, validator, entryPoint } = await setupTests() + const feeBeneficiary = ethers.Wallet.createRandom().address + const randomAddress = ethers.Wallet.createRandom().address + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.eq(0) await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('1.0')) const safeOp = buildSafeUserOpTransaction( safe.address, - user1.address, + randomAddress, ethers.parseEther('0.5'), '0x', '0', @@ -174,8 +179,12 @@ describe('Safe4337Module - Newly deployed safe', () => { safeOp, signature, }) - await logGas('Execute UserOp with fee payment', entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001'))) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.499999')) + await logGas('Execute UserOp with fee payment', entryPoint.handleOps([userOp], feeBeneficiary)) + + // checking that the fee was paid + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.gt(0) + // check that the call was executed + expect(await ethers.provider.getBalance(randomAddress)).to.be.eq(ethers.parseEther('0.5')) }) it('executeUserOpWithErrorString should execute contract calls', async () => { @@ -194,6 +203,7 @@ describe('Safe4337Module - Newly deployed safe', () => { true, { initCode: safe.getInitCode(), + maxFeePerGas: 0, }, ) const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())]) @@ -201,11 +211,8 @@ describe('Safe4337Module - Newly deployed safe', () => { safeOp, signature, }) - await logGas( - 'Execute UserOp with fee payment and bubble up error string', - entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001')), - ) - expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.499999')) + await logGas('Execute UserOp with fee payment and bubble up error string', entryPoint.handleOps([userOp], user1.address)) + expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0.5')) }) it('executeUserOpWithErrorString reverts on failure and bubbles up the revert reason', async () => { @@ -234,14 +241,14 @@ describe('Safe4337Module - Newly deployed safe', () => { safeOp, signature, }) - const transaction = await logGas('Execute UserOp without fee payment', entryPoint.executeUserOp(userOp, 0)) - const receipt = await transaction.wait() - const logs = receipt.logs.map((log) => entryPoint.interface.parseLog(log)) - const emittedRevert = logs.find((log) => log?.name === 'UserOpReverted') - expect(emittedRevert?.args.reason).to.equal( - reverterContract.interface.encodeErrorResult('Error', ['You called a function that always reverts']), - ) - expect(await safe.isDeployed()).to.be.true + + // Error('You called a function that always reverts') + const expectedRevertReason = validator.interface.encodeErrorResult('Error(string)', ['You called a function that always reverts']) + await expect(entryPoint.handleOps([userOp], user1.address)) + .to.emit(entryPoint, 'UserOperationRevertReason') + .withArgs(await entryPoint.getUserOpHash(userOp), safeOp.safe, 0, expectedRevertReason) + + await expect(ethers.provider.getCode(safe.address)).to.not.eq('0x') }) }) }) diff --git a/modules/4337/test/erc4337/ERC4337WebAuthn.spec.ts b/modules/4337/test/erc4337/ERC4337WebAuthn.spec.ts index d813109f..bc484241 100644 --- a/modules/4337/test/erc4337/ERC4337WebAuthn.spec.ts +++ b/modules/4337/test/erc4337/ERC4337WebAuthn.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import { deployments, ethers } from 'hardhat' -import { deployReferenceEntryPoint } from '../utils/setup' +import { getEntryPoint } from '../utils/setup' import { buildContractSignatureBytes, logGas } from '../../src/utils/execution' import { buildSafeUserOpTransaction, buildUserOperationFromSafeUserOperation, calculateSafeOperationHash } from '../../src/utils/userOp' import { chainId } from '../utils/encoding' @@ -17,8 +17,8 @@ describe('Safe4337Module - WebAuthn Owner', () => { const setupTests = deployments.createFixture(async ({ deployments }) => { const { SafeModuleSetup, SafeL2, SafeProxyFactory, WebAuthnVerifier } = await deployments.fixture() - const [deployer, relayer, user] = await ethers.getSigners() - const entryPoint = await deployReferenceEntryPoint(deployer, relayer) + const [user] = await ethers.getSigners() + const entryPoint = await getEntryPoint() const moduleFactory = await ethers.getContractFactory('Safe4337Module') const module = await moduleFactory.deploy(entryPoint.target) const proxyFactory = await ethers.getContractAt('SafeProxyFactory', SafeProxyFactory.address) diff --git a/modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts b/modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts index ea8d422f..81a9e242 100644 --- a/modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts +++ b/modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import { deployments, ethers } from 'hardhat' import { time } from '@nomicfoundation/hardhat-network-helpers' import { EventLog, Log } from 'ethers' -import { deployReferenceEntryPoint, getFactory, getSafeModuleSetup } from '../utils/setup' +import { getEntryPoint, getFactory, getSafeModuleSetup } from '../utils/setup' import { buildContractSignatureBytes, buildSignatureBytes, logGas } from '../../src/utils/execution' import { buildSafeUserOpTransaction, @@ -16,9 +16,9 @@ import { Safe4337 } from '../../src/utils/safe' describe('Safe4337Module - Reference EntryPoint', () => { const setupTests = async () => { await deployments.fixture() - const [deployer, user, relayer] = await ethers.getSigners() + const [user, deployer, relayer] = await ethers.getSigners() - const entryPoint = await deployReferenceEntryPoint(deployer, relayer) + const entryPoint = await getEntryPoint() const moduleFactory = await ethers.getContractFactory('Safe4337Module') const module = await moduleFactory.deploy(await entryPoint.getAddress()) const proxyFactory = await getFactory() diff --git a/modules/4337/test/erc4337/Safe4337Mock.spec.ts b/modules/4337/test/erc4337/Safe4337Mock.spec.ts index 2ea0cb41..37b50a1b 100644 --- a/modules/4337/test/erc4337/Safe4337Mock.spec.ts +++ b/modules/4337/test/erc4337/Safe4337Mock.spec.ts @@ -40,22 +40,30 @@ describe('Safe4337Mock', () => { '0x', '0', await entryPoint.getAddress(), + false, + false, + { + maxFeePerGas: '0', + }, ) const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await logGas('Execute UserOp without fee payment', entryPoint.executeUserOp(userOp, 0)) + await logGas('Execute UserOp without fee payment', entryPoint.handleOps([userOp], user1.address)) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.5')) }) it('should execute contract calls with fee', async () => { const { user1, safe, validator, entryPoint } = await setupTests() + const feeBeneficiary = ethers.Wallet.createRandom().address + const randomAddress = ethers.Wallet.createRandom().address + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.eq(0) await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther('1.0') }) expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('1.0')) const safeOp = buildSafeUserOpTransaction( await safe.getAddress(), - user1.address, + randomAddress, ethers.parseEther('0.5'), '0x', '0', @@ -64,8 +72,12 @@ describe('Safe4337Mock', () => { const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId()) const signature = buildSignatureBytes([await signHash(user1, safeOpHash)]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) - await logGas('Execute UserOp with fee payment', entryPoint.executeUserOp(userOp, ethers.parseEther('0.000001'))) - expect(await ethers.provider.getBalance(await safe.getAddress())).to.be.eq(ethers.parseEther('0.499999')) + await logGas('Execute UserOp with fee payment', entryPoint.handleOps([userOp], feeBeneficiary)) + + // checking that the fee was paid + expect(await ethers.provider.getBalance(feeBeneficiary)).to.be.gt(0) + // check that the call was executed + expect(await ethers.provider.getBalance(randomAddress)).to.be.eq(ethers.parseEther('0.5')) }) }) diff --git a/modules/4337/test/gas/Gas.spec.ts b/modules/4337/test/gas/Gas.spec.ts index d4623d83..02edbeb4 100644 --- a/modules/4337/test/gas/Gas.spec.ts +++ b/modules/4337/test/gas/Gas.spec.ts @@ -44,6 +44,8 @@ describe('Gas Metering', () => { it('Safe with 4337 Module Deployment', async () => { const { user, entryPoint, validator, safe } = await setupTests() + // cover the prefund + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) const safeOp = buildSafeUserOpTransaction( @@ -67,7 +69,7 @@ describe('Gas Metering', () => { signature, }) - await logGas('Safe with 4337 Module Deployment', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module Deployment', entryPoint.handleOps([userOp], user.address)) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) }) @@ -77,18 +79,17 @@ describe('Gas Metering', () => { it('Safe with 4337 Module Deployment + Native Transfer', async () => { const { user, entryPoint, validator, safe } = await setupTests() const amount = ethers.parseEther('0.00001') + const receiver = ethers.Wallet.createRandom().address expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) await user.sendTransaction({ to: safe.address, - value: amount, + value: ethers.parseEther('1'), }) - const safeBalBefore = await ethers.provider.getBalance(safe.address) - expect(safeBalBefore).to.equal(amount) const safeOp = buildSafeUserOpTransaction( safe.address, - user.address, + receiver, amount, '0x', await entryPoint.getNonce(safe.address, 0), @@ -107,71 +108,44 @@ describe('Gas Metering', () => { signature, }) - await logGas('Safe with 4337 Module Deployment + Native Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module Deployment + Native Transfer', entryPoint.handleOps([userOp], user.address)) - const safeBalAfter = await ethers.provider.getBalance(safe.address) + const recipientBalAfter = await ethers.provider.getBalance(receiver) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) - expect(safeBalAfter).to.equal(0) + expect(recipientBalAfter).to.equal(amount) }) it('Safe with 4337 Module Native Transfer', async () => { const { user, entryPoint, validator, safe } = await setupTests() + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) - let safeOp = buildSafeUserOpTransaction( - safe.address, - safe.address, - 0, - '0x', - await entryPoint.getNonce(safe.address, 0), - await entryPoint.getAddress(), - false, - false, - { - initCode: safe.getInitCode(), - }, - ) - let signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - let userOp = buildUserOperationFromSafeUserOperation({ - safeOp, - signature, - }) - - await entryPoint.executeUserOp(userOp, 0) + await safe.deploy(user) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) // Now Native Transfer const amount = ethers.parseEther('0.00001') - expect(await ethers.provider.getBalance(safe.address)).to.equal(0) - await user.sendTransaction({ - to: safe.address, - value: amount, - }) - expect(await ethers.provider.getBalance(safe.address)).to.equal(amount) - - safeOp = buildSafeUserOpTransaction( + const receiver = ethers.Wallet.createRandom().address + const safeOp = buildSafeUserOpTransaction( safe.address, - user.address, + receiver, amount, '0x', await entryPoint.getNonce(safe.address, 0), await entryPoint.getAddress(), false, false, - { - initCode: safe.getInitCode(), - }, ) - signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - userOp = buildUserOperationFromSafeUserOperation({ + const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) + const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, }) - await logGas('Safe with 4337 Module Native Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module Native Transfer', entryPoint.handleOps([userOp], user.address)) - expect(await ethers.provider.getBalance(safe.address)).to.equal(0) + expect(await ethers.provider.getBalance(receiver)).to.equal(amount) }) }) @@ -179,6 +153,8 @@ describe('Gas Metering', () => { it('Safe with 4337 Module Deployment + ERC20 Token Transfer', async () => { const { user, entryPoint, validator, safe, erc20Token } = await setupTests() + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) + expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) expect(await erc20Token.balanceOf(safe.address)).to.equal(0) @@ -206,7 +182,7 @@ describe('Gas Metering', () => { signature, }) - await logGas('Safe with 4337 Module Deployment + ERC20 Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module Deployment + ERC20 Transfer', entryPoint.handleOps([userOp], user.address)) expect(await erc20Token.balanceOf(safe.address)).to.equal(0) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) }) @@ -215,6 +191,8 @@ describe('Gas Metering', () => { const { user, entryPoint, validator, safe, erc721Token } = await setupTests() const tokenID = 1 + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) + expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) const safeOp = buildSafeUserOpTransaction( @@ -237,7 +215,7 @@ describe('Gas Metering', () => { }) expect(await erc721Token.balanceOf(safe.address)).to.equal(0) - await logGas('Safe with 4337 Module Deployment + ERC721 Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module Deployment + ERC721 Transfer', entryPoint.handleOps([userOp], user.address)) expect(await erc721Token.balanceOf(safe.address)).to.equal(1) expect(await erc721Token.ownerOf(tokenID)).to.equal(safe.address) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) @@ -248,28 +226,11 @@ describe('Gas Metering', () => { it('Safe with 4337 Module ERC20 Token Transfer', async () => { const { user, entryPoint, validator, safe, erc20Token } = await setupTests() - expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) - let safeOp = buildSafeUserOpTransaction( - safe.address, - safe.address, // No functions are called. - 0, - '0x', - await entryPoint.getNonce(safe.address, 0), - await entryPoint.getAddress(), - false, - false, - { - initCode: safe.getInitCode(), - }, - ) - let signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - let userOp = buildUserOperationFromSafeUserOperation({ - safeOp, - signature, - }) + expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) - await entryPoint.executeUserOp(userOp, 0) + await safe.deploy(user) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) // Now Token Transfer @@ -277,7 +238,7 @@ describe('Gas Metering', () => { await erc20Token.transfer(safe.address, ethers.parseUnits('4.2', 18)).then((tx) => tx.wait()) expect(await erc20Token.balanceOf(safe.address)).to.equal(ethers.parseUnits('4.2', 18)) - safeOp = buildSafeUserOpTransaction( + const safeOp = buildSafeUserOpTransaction( safe.address, await erc20Token.getAddress(), 0, @@ -286,17 +247,14 @@ describe('Gas Metering', () => { await entryPoint.getAddress(), false, false, - { - initCode: safe.getInitCode(), - }, ) - signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - userOp = buildUserOperationFromSafeUserOperation({ + const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) + const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, }) - await logGas('Safe with 4337 Module ERC20 Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module ERC20 Transfer', entryPoint.handleOps([userOp], user.address)) expect(await erc20Token.balanceOf(safe.address)).to.equal(0) }) @@ -304,34 +262,17 @@ describe('Gas Metering', () => { it('Safe with 4337 Module ERC721 Token Minting', async () => { const { user, entryPoint, validator, safe, erc721Token } = await setupTests() - expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') }) - let safeOp = buildSafeUserOpTransaction( - safe.address, - safe.address, // No functions are called. - 0, - '0x', - await entryPoint.getNonce(safe.address, 0), - await entryPoint.getAddress(), - false, - false, - { - initCode: safe.getInitCode(), - }, - ) - let signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - let userOp = buildUserOperationFromSafeUserOperation({ - safeOp, - signature, - }) + expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.equal(0) - await entryPoint.executeUserOp(userOp, 0) + await safe.deploy(user) expect(ethers.dataLength(await ethers.provider.getCode(safe.address))).to.not.equal(0) // Now ERC721 Token Transfer const tokenID = 1 - safeOp = buildSafeUserOpTransaction( + const safeOp = buildSafeUserOpTransaction( safe.address, await erc721Token.getAddress(), 0, @@ -340,18 +281,15 @@ describe('Gas Metering', () => { await entryPoint.getAddress(), false, false, - { - initCode: safe.getInitCode(), - }, ) - signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) - userOp = buildUserOperationFromSafeUserOperation({ + const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())]) + const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, }) expect(await erc721Token.balanceOf(safe.address)).to.equal(0) - await logGas('Safe with 4337 Module ERC721 Transfer', entryPoint.executeUserOp(userOp, 0)) + await logGas('Safe with 4337 Module ERC721 Transfer', entryPoint.handleOps([userOp], user.address)) expect(await erc721Token.balanceOf(safe.address)).to.equal(1) expect(await erc721Token.ownerOf(tokenID)).to.equal(safe.address) }) diff --git a/modules/4337/test/utils/setup.ts b/modules/4337/test/utils/setup.ts index f0847527..0b3a8e5f 100644 --- a/modules/4337/test/utils/setup.ts +++ b/modules/4337/test/utils/setup.ts @@ -1,9 +1,8 @@ -import EntryPointArtifact from '@account-abstraction/contracts/artifacts/EntryPoint.json' import { deployments, ethers } from 'hardhat' import { Contract, Signer } from 'ethers' import solc from 'solc' import { logGas } from '../../src/utils/execution' -import { Safe4337Mock, SafeMock, IEntryPoint } from '../../typechain-types' +import { Safe4337Mock, SafeMock } from '../../typechain-types' const getRandomInt = (min = 0, max: number = Number.MAX_SAFE_INTEGER): number => { return Math.floor(Math.random() * (max - min + 1)) + min @@ -51,7 +50,7 @@ export const getSafe4337Module = async () => { export const getEntryPoint = async () => { const EntryPointDeployment = await deployments.get('EntryPoint') - return await ethers.getContractAt('TestEntryPoint', EntryPointDeployment.address) + return await ethers.getContractAt('IEntryPoint', EntryPointDeployment.address) } export const getSafeAtAddress = async (address: string) => { @@ -113,14 +112,3 @@ export const deployContract = async (deployer: Signer, source: string): Promise< } return new Contract(contractAddress, output.interface, deployer) } - -export const deployReferenceEntryPoint = async (deployer: Signer, relayer?: Signer) => { - const { abi, bytecode } = EntryPointArtifact - const transaction = await deployer.sendTransaction({ data: bytecode }) - const receipt = await transaction.wait() - const contractAddress = receipt.contractAddress - if (contractAddress === null) { - throw new Error(`contract deployment transaction ${transaction.hash} missing address`) - } - return new ethers.Contract(contractAddress, abi, relayer) as unknown as IEntryPoint -}