From e1d44e0342ab6d8d884d0c8c48cfc55ac10fb5ec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 25 Nov 2024 18:35:09 +0100 Subject: [PATCH] Add `factory()`, `factoryData()` and `paymasterData()` helpers to ERC4337Utils (#5313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- .../account/utils/draft-ERC4337Utils.sol | 29 ++++++++- test/account/utils/draft-ERC4337Utils.test.js | 62 ++++++++++++++++--- test/account/utils/draft-ERC7579Utils.test.js | 5 +- test/helpers/erc4337.js | 32 +++++++--- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol index 7f72d3404..2caa3bd21 100644 --- a/contracts/account/utils/draft-ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -98,6 +98,16 @@ library ERC4337Utils { return result; } + /// @dev Returns `factory` from the {PackedUserOperation}, or address(0) if the initCode is empty or not properly formatted. + function factory(PackedUserOperation calldata self) internal pure returns (address) { + return self.initCode.length < 20 ? address(0) : address(bytes20(self.initCode[0:20])); + } + + /// @dev Returns `factoryData` from the {PackedUserOperation}, or empty bytes if the initCode is empty or not properly formatted. + function factoryData(PackedUserOperation calldata self) internal pure returns (bytes calldata) { + return self.initCode.length < 20 ? _emptyCalldataBytes() : self.initCode[20:]; + } + /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}. function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { return uint128(self.accountGasLimits.extract_32_16(0x00)); @@ -130,16 +140,29 @@ library ERC4337Utils { /// @dev Returns the first section of `paymasterAndData` from the {PackedUserOperation}. function paymaster(PackedUserOperation calldata self) internal pure returns (address) { - return address(bytes20(self.paymasterAndData[0:20])); + return self.paymasterAndData.length < 52 ? address(0) : address(bytes20(self.paymasterAndData[0:20])); } /// @dev Returns the second section of `paymasterAndData` from the {PackedUserOperation}. function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(bytes16(self.paymasterAndData[20:36])); + return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[20:36])); } /// @dev Returns the third section of `paymasterAndData` from the {PackedUserOperation}. function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { - return uint128(bytes16(self.paymasterAndData[36:52])); + return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[36:52])); + } + + /// @dev Returns the forth section of `paymasterAndData` from the {PackedUserOperation}. + function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) { + return self.paymasterAndData.length < 52 ? _emptyCalldataBytes() : self.paymasterAndData[52:]; + } + + // slither-disable-next-line write-after-write + function _emptyCalldataBytes() private pure returns (bytes calldata result) { + assembly ("memory-safe") { + result.offset := 0 + result.length := 0 + } } } diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js index 5138a5d8e..96310ed83 100644 --- a/test/account/utils/draft-ERC4337Utils.test.js +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -2,13 +2,13 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { packValidationData, packPaymasterData, UserOperation } = require('../../helpers/erc4337'); +const { packValidationData, UserOperation } = require('../../helpers/erc4337'); const { MAX_UINT48 } = require('../../helpers/constants'); const fixture = async () => { - const [authorizer, sender, entrypoint, paymaster] = await ethers.getSigners(); + const [authorizer, sender, entrypoint, factory, paymaster] = await ethers.getSigners(); const utils = await ethers.deployContract('$ERC4337Utils'); - return { utils, authorizer, sender, entrypoint, paymaster }; + return { utils, authorizer, sender, entrypoint, factory, paymaster }; }; describe('ERC4337Utils', function () { @@ -144,6 +144,33 @@ describe('ERC4337Utils', function () { }); describe('userOp values', function () { + describe('intiCode', function () { + beforeEach(async function () { + this.userOp = new UserOperation({ + sender: this.sender, + nonce: 1, + verificationGas: 0x12345678n, + factory: this.factory, + factoryData: '0x123456', + }); + + this.emptyUserOp = new UserOperation({ + sender: this.sender, + nonce: 1, + }); + }); + + it('returns factory', async function () { + expect(this.utils.$factory(this.userOp.packed)).to.eventually.equal(this.factory); + expect(this.utils.$factory(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress); + }); + + it('returns factoryData', async function () { + expect(this.utils.$factoryData(this.userOp.packed)).to.eventually.equal('0x123456'); + expect(this.utils.$factoryData(this.emptyUserOp.packed)).to.eventually.equal('0x'); + }); + }); + it('returns verificationGasLimit', async function () { const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n }); expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas); @@ -176,28 +203,43 @@ describe('ERC4337Utils', function () { describe('paymasterAndData', function () { beforeEach(async function () { - this.verificationGasLimit = 0x12345678n; - this.postOpGasLimit = 0x87654321n; - this.paymasterAndData = packPaymasterData(this.paymaster, this.verificationGasLimit, this.postOpGasLimit); this.userOp = new UserOperation({ sender: this.sender, nonce: 1, - paymasterAndData: this.paymasterAndData, + paymaster: this.paymaster, + paymasterVerificationGasLimit: 0x12345678n, + paymasterPostOpGasLimit: 0x87654321n, + paymasterData: '0xbeefcafe', + }); + + this.emptyUserOp = new UserOperation({ + sender: this.sender, + nonce: 1, }); }); it('returns paymaster', async function () { - expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.paymaster); + expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.userOp.paymaster); + expect(this.utils.$paymaster(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress); }); it('returns verificationGasLimit', async function () { expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal( - this.verificationGasLimit, + this.userOp.paymasterVerificationGasLimit, ); + expect(this.utils.$paymasterVerificationGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n); }); it('returns postOpGasLimit', async function () { - expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal(this.postOpGasLimit); + expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal( + this.userOp.paymasterPostOpGasLimit, + ); + expect(this.utils.$paymasterPostOpGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n); + }); + + it('returns data', async function () { + expect(this.utils.$paymasterData(this.userOp.packed)).to.eventually.equal(this.userOp.paymasterData); + expect(this.utils.$paymasterData(this.emptyUserOp.packed)).to.eventually.equal('0x'); }); }); }); diff --git a/test/account/utils/draft-ERC7579Utils.test.js b/test/account/utils/draft-ERC7579Utils.test.js index cc3b70425..e72b6698d 100644 --- a/test/account/utils/draft-ERC7579Utils.test.js +++ b/test/account/utils/draft-ERC7579Utils.test.js @@ -1,6 +1,6 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { loadFixture, setBalance } = require('@nomicfoundation/hardhat-network-helpers'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY, @@ -17,11 +17,10 @@ const coder = ethers.AbiCoder.defaultAbiCoder(); const fixture = async () => { const [sender] = await ethers.getSigners(); - const utils = await ethers.deployContract('$ERC7579Utils'); + const utils = await ethers.deployContract('$ERC7579Utils', { value: ethers.parseEther('1') }); const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock'); const target = await ethers.deployContract('CallReceiverMock'); const anotherTarget = await ethers.deployContract('CallReceiverMock'); - await setBalance(utils.target, ethers.parseEther('1')); return { utils, utilsGlobal, target, anotherTarget, sender }; }; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js index 5901375b1..50a3b4a04 100644 --- a/test/helpers/erc4337.js +++ b/test/helpers/erc4337.js @@ -26,10 +26,14 @@ function packValidationData(validAfter, validUntil, authorizer) { ); } -function packPaymasterData(paymaster, verificationGasLimit, postOpGasLimit) { +function packInitCode(factory, factoryData) { + return ethers.solidityPacked(['address', 'bytes'], [getAddress(factory), factoryData]); +} + +function packPaymasterAndData(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData) { return ethers.solidityPacked( - ['address', 'uint128', 'uint128'], - [getAddress(paymaster), verificationGasLimit, postOpGasLimit], + ['address', 'uint128', 'uint128', 'bytes'], + [getAddress(paymaster), paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData], ); } @@ -38,14 +42,18 @@ class UserOperation { constructor(params) { this.sender = getAddress(params.sender); this.nonce = params.nonce; - this.initCode = params.initCode ?? '0x'; + this.factory = params.factory ?? undefined; + this.factoryData = params.factoryData ?? '0x'; this.callData = params.callData ?? '0x'; this.verificationGas = params.verificationGas ?? 10_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.paymaster = params.paymaster ?? undefined; + this.paymasterVerificationGasLimit = params.paymasterVerificationGasLimit ?? 0n; + this.paymasterPostOpGasLimit = params.paymasterPostOpGasLimit ?? 0n; + this.paymasterData = params.paymasterData ?? '0x'; this.signature = params.signature ?? '0x'; } @@ -53,12 +61,19 @@ class UserOperation { return { sender: this.sender, nonce: this.nonce, - initCode: this.initCode, + initCode: this.factory ? packInitCode(this.factory, this.factoryData) : '0x', callData: this.callData, accountGasLimits: pack(this.verificationGas, this.callGas), preVerificationGas: this.preVerificationGas, gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), - paymasterAndData: this.paymasterAndData, + paymasterAndData: this.paymaster + ? packPaymasterAndData( + this.paymaster, + this.paymasterVerificationGasLimit, + this.paymasterPostOpGasLimit, + this.paymasterData, + ) + : '0x', signature: this.signature, }; } @@ -90,6 +105,7 @@ module.exports = { SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILURE, packValidationData, - packPaymasterData, + packInitCode, + packPaymasterAndData, UserOperation, };