From 30c2d1248034fe1a4f412d8fdf868132cc64a98c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 29 Apr 2024 12:19:15 +0200 Subject: [PATCH] update 4337 helper --- contracts/abstraction/utils/ERC4337Utils.sol | 114 +++++-------------- test/abstraction/entrypoint.test.js | 112 +++++++++++++----- test/helpers/erc4337.js | 31 +++-- 3 files changed, 130 insertions(+), 127 deletions(-) diff --git a/contracts/abstraction/utils/ERC4337Utils.sol b/contracts/abstraction/utils/ERC4337Utils.sol index c50696e076..429d7e8494 100644 --- a/contracts/abstraction/utils/ERC4337Utils.sol +++ b/contracts/abstraction/utils/ERC4337Utils.sol @@ -56,93 +56,37 @@ library ERC4337Utils { } } - /* - enum ErrorCodes { - AA10_SENDER_ALREADY_CONSTRUCTED, - AA13_INITCODE_FAILLED, - AA14_INITCODE_WRONG_SENDER, - AA15_INITCODE_NO_DEPLOYMENT, - // Account - AA21_MISSING_FUNDS, - AA22_EXPIRED_OR_NOT_DUE, - AA23_REVERTED, - AA24_SIGNATURE_ERROR, - AA25_INVALID_NONCE, - AA26_OVER_VERIFICATION_GAS_LIMIT, - // Paymaster - AA31_MISSING_FUNDS, - AA32_EXPIRED_OR_NOT_DUE, - AA33_REVERTED, - AA34_SIGNATURE_ERROR, - AA36_OVER_VERIFICATION_GAS_LIMIT, - // other - AA95_OUT_OF_GAS - } - - function toString(ErrorCodes err) internal pure returns (string memory) { - if (err == ErrorCodes.AA10_SENDER_ALREADY_CONSTRUCTED) { - return "AA10 sender already constructed"; - } else if (err == ErrorCodes.AA13_INITCODE_FAILLED) { - return "AA13 initCode failed or OOG"; - } else if (err == ErrorCodes.AA14_INITCODE_WRONG_SENDER) { - return "AA14 initCode must return sender"; - } else if (err == ErrorCodes.AA15_INITCODE_NO_DEPLOYMENT) { - return "AA15 initCode must create sender"; - } else if (err == ErrorCodes.AA21_MISSING_FUNDS) { - return "AA21 didn't pay prefund"; - } else if (err == ErrorCodes.AA22_EXPIRED_OR_NOT_DUE) { - return "AA22 expired or not due"; - } else if (err == ErrorCodes.AA23_REVERTED) { - return "AA23 reverted"; - } else if (err == ErrorCodes.AA24_SIGNATURE_ERROR) { - return "AA24 signature error"; - } else if (err == ErrorCodes.AA25_INVALID_NONCE) { - return "AA25 invalid account nonce"; - } else if (err == ErrorCodes.AA26_OVER_VERIFICATION_GAS_LIMIT) { - return "AA26 over verificationGasLimit"; - } else if (err == ErrorCodes.AA31_MISSING_FUNDS) { - return "AA31 paymaster deposit too low"; - } else if (err == ErrorCodes.AA32_EXPIRED_OR_NOT_DUE) { - return "AA32 paymaster expired or not due"; - } else if (err == ErrorCodes.AA33_REVERTED) { - return "AA33 reverted"; - } else if (err == ErrorCodes.AA34_SIGNATURE_ERROR) { - return "AA34 signature error"; - } else if (err == ErrorCodes.AA36_OVER_VERIFICATION_GAS_LIMIT) { - return "AA36 over paymasterVerificationGasLimit"; - } else if (err == ErrorCodes.AA95_OUT_OF_GAS) { - return "AA95 out of gas"; - } else { - return "Unknown error code"; - } - } - - function failedOp(uint256 index, ErrorCodes err) internal pure { - revert IEntryPoint.FailedOp(index, toString(err)); - } - - function failedOp(uint256 index, ErrorCodes err, bytes memory extraData) internal pure { - revert IEntryPoint.FailedOpWithRevert(index, toString(err), extraData); - } - */ - // Packed user operation - function hash(PackedUserOperation calldata self) internal pure returns (bytes32) { - return keccak256(encode(self)); + function hash(PackedUserOperation calldata self) internal view returns (bytes32) { + return hash(self, address(this), block.chainid); } - function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) { - return + function hash( + PackedUserOperation calldata self, + address entrypoint, + uint256 chainid + ) internal pure returns (bytes32) { + Memory.FreePtr ptr = Memory.save(); + bytes32 result = keccak256( abi.encode( - self.sender, - self.nonce, - keccak256(self.initCode), - keccak256(self.callData), - self.accountGasLimits, - self.preVerificationGas, - self.gasFees, - keccak256(self.paymasterAndData) - ); + keccak256( + abi.encode( + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) + ) + ), + entrypoint, + chainid + ) + ); + Memory.load(ptr); + return result; } function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { @@ -199,7 +143,6 @@ 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(); @@ -216,11 +159,10 @@ library ERC4337Utils { self.paymasterVerificationGasLimit = 0; self.paymasterPostOpGasLimit = 0; } - self.userOpHash = keccak256(abi.encode(hash(source), address(this), block.chainid)); + self.userOpHash = hash(source); 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 278cfd184e..1b796d2e9b 100644 --- a/test/abstraction/entrypoint.test.js +++ b/test/abstraction/entrypoint.test.js @@ -3,50 +3,100 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const { ERC4337Context } = require('../helpers/erc4337'); +const { ERC4337Helper } = require('../helpers/erc4337'); async function fixture() { const accounts = await ethers.getSigners(); - const context = new ERC4337Context(); - await context.wait(); + const helper = new ERC4337Helper(); + await helper.wait(); return { accounts, - context, - entrypoint: context.entrypoint, - factory: context.factory, + helper, + entrypoint: helper.entrypoint, + factory: helper.factory, }; } describe('EntryPoint', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); + + this.user = this.accounts.shift(); + this.beneficiary = this.accounts.shift(); + this.sender = await this.helper.newAccount(this.user); }); - it('', async function () { - const user = this.accounts[0]; - const beneficiary = this.accounts[1]; - 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') }); - - 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') - .withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress) - .to.emit(this.entrypoint, 'Transfer') - .withArgs(ethers.ZeroAddress, sender, anyValue) - .to.emit(this.entrypoint, 'BeforeExecution') - // 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'); + describe('deploy wallet contract', function () { + it('success: counterfactual funding', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.emit(this.sender, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.user) + .to.emit(this.factory, 'return$deploy') + .withArgs(this.sender) + .to.emit(this.entrypoint, 'AccountDeployed') + .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + .to.emit(this.entrypoint, 'Transfer') + .withArgs(ethers.ZeroAddress, this.sender, anyValue) + .to.emit(this.entrypoint, 'BeforeExecution') + // BeforeExecution has no args + .to.emit(this.entrypoint, 'UserOperationEvent') + .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it.skip('[TODO] success: paymaster funding', async function () { + // TODO: deploy paymaster + // TODO: fund paymaster's account in entrypoint + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + // const operation = await this.sender.createOp({ paymaster: this.user }, true); + // await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + // .to.emit(this.sender, 'OwnershipTransferred') + // .withArgs(ethers.ZeroAddress, this.user) + // .to.emit(this.factory, 'return$deploy') + // .withArgs(this.sender) + // .to.emit(this.entrypoint, 'AccountDeployed') + // .withArgs(operation.hash, this.sender, this.factory, ethers.ZeroAddress) + // .to.emit(this.entrypoint, 'Transfer') + // .withArgs(ethers.ZeroAddress, this.sender, anyValue) + // .to.emit(this.entrypoint, 'BeforeExecution') + // // BeforeExecution has no args + // .to.emit(this.entrypoint, 'UserOperationEvent') + // .withArgs(operation.hash, this.sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue); + + expect(await ethers.provider.getCode(this.sender)).to.not.equal('0x'); + }); + + it("error: AA21 didn't pay prefund", async function () { + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({}, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, "AA21 didn't pay prefund"); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); + + it('error: AA25 invalid account nonce', async function () { + await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') }); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + + const operation = await this.sender.createOp({ nonce: 1n }, true); + await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary)) + .to.be.revertedWithCustomError(this.entrypoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce'); + + expect(await ethers.provider.getCode(this.sender)).to.equal('0x'); + }); }); }); diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 0375b27198..37e1c2c9ce 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); function pack(left, right) { - return ethers.toBeHex((left << 128n) | right, 32); + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); } -class ERC4337Context { +class ERC4337Helper { constructor() { this.entrypointAsPromise = ethers.deployContract('EntryPoint'); this.factoryAsPromise = ethers.deployContract('$Create2'); @@ -41,19 +41,30 @@ class AbstractAccount extends ethers.BaseContract { this.context = context; } - createOp(params = {}, withInit = false) { - return new UserOperation({ - ...params, - sender: this, - initCode: withInit ? this.initCode : '0x', - }); + async createOp(args = {}, withInit = false) { + const params = Object.assign({ sender: this, initCode: withInit ? this.initCode : '0x' }, args); + // fetch nonce + if (!params.nonce) { + params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0)); + } + // prepare paymaster and data + if (ethers.isAddressable(params.paymaster)) { + params.paymaster = await ethers.resolveAddress(params.paymaster); + params.paymasterVerificationGasLimit ??= 100_000n; + params.paymasterPostOpGasLimit ??= 100_000n; + params.paymasterAndData = ethers.solidityPacked( + ['address', 'uint128', 'uint128'], + [params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit], + ); + } + return new UserOperation(params); } } class UserOperation { constructor(params) { this.sender = params.sender; - this.nonce = params.nonce ?? 0n; + this.nonce = params.nonce; this.initCode = params.initCode ?? '0x'; this.callData = params.callData ?? '0x'; this.verificationGas = params.verificationGas ?? 2_000_000n; @@ -106,7 +117,7 @@ class UserOperation { } module.exports = { - ERC4337Context, + ERC4337Helper, AbstractAccount, UserOperation, };