diff --git a/contracts/core/BaseAccount.sol b/contracts/core/BaseAccount.sol index 5c89e0f4..45b6793d 100644 --- a/contracts/core/BaseAccount.sol +++ b/contracts/core/BaseAccount.sol @@ -15,7 +15,7 @@ import "./UserOperationLib.sol"; * Specific account implementation should inherit it and provide the account-specific logic. */ abstract contract BaseAccount is IAccount { - using UserOperationLib for UserOperation; + using UserOperationLib for PackedUserOperation; /** * Return value in case of signature failure, with no time-range. @@ -48,7 +48,7 @@ abstract contract BaseAccount is IAccount { * to pay for the user operation. */ function validateUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external virtual override returns (uint256 validationData) { @@ -83,7 +83,7 @@ abstract contract BaseAccount is IAccount { * Note that the validation code cannot use block.timestamp (or block.number) directly. */ function _validateSignature( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash ) internal virtual returns (uint256 validationData); diff --git a/contracts/core/BasePaymaster.sol b/contracts/core/BasePaymaster.sol index dee42b2d..efb8b9cb 100644 --- a/contracts/core/BasePaymaster.sol +++ b/contracts/core/BasePaymaster.sol @@ -8,7 +8,7 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "../interfaces/IPaymaster.sol"; import "../interfaces/IEntryPoint.sol"; import "./Helpers.sol"; - +import "./UserOperationLib.sol"; /** * Helper class for creating a paymaster. * provides helper methods for staking. @@ -17,6 +17,10 @@ import "./Helpers.sol"; abstract contract BasePaymaster is IPaymaster, Ownable { IEntryPoint public immutable entryPoint; + uint256 internal constant PAYMASTER_VALIDATION_GAS_OFFSET = UserOperationLib.PAYMASTER_VALIDATION_GAS_OFFSET; + uint256 internal constant PAYMASTER_POSTOP_GAS_OFFSET = UserOperationLib.PAYMASTER_POSTOP_GAS_OFFSET; + uint256 internal constant PAYMASTER_DATA_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; + constructor(IEntryPoint _entryPoint) Ownable(msg.sender) { _validateEntryPointInterface(_entryPoint); entryPoint = _entryPoint; @@ -30,7 +34,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable { /// @inheritdoc IPaymaster function validatePaymasterUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost ) external override returns (bytes memory context, uint256 validationData) { @@ -45,7 +49,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable { * @param maxCost - The maximum cost of the user operation. */ function _validatePaymasterUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost ) internal virtual returns (bytes memory context, uint256 validationData); diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 0a6cc946..0a3994bb 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -25,7 +25,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; */ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, OpenZeppelin.ERC165 { - using UserOperationLib for UserOperation; + using UserOperationLib for PackedUserOperation; SenderCreator private senderCreator = new SenderCreator(); @@ -71,7 +71,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, */ function _executeUserOp( uint256 opIndex, - UserOperation calldata userOp, + PackedUserOperation calldata userOp, UserOpInfo memory opInfo ) internal @@ -141,7 +141,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, /// @inheritdoc IEntryPoint function handleOps( - UserOperation[] calldata ops, + PackedUserOperation[] calldata ops, address payable beneficiary ) public nonReentrant { uint256 opslen = ops.length; @@ -183,7 +183,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, uint256 totalOps = 0; for (uint256 i = 0; i < opasLen; i++) { UserOpsPerAggregator calldata opa = opsPerAggregator[i]; - UserOperation[] calldata ops = opa.userOps; + PackedUserOperation[] calldata ops = opa.userOps; IAggregator aggregator = opa.aggregator; //address(1) is special marker of "signature error" @@ -207,7 +207,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, uint256 opIndex = 0; for (uint256 a = 0; a < opasLen; a++) { UserOpsPerAggregator calldata opa = opsPerAggregator[a]; - UserOperation[] calldata ops = opa.userOps; + PackedUserOperation[] calldata ops = opa.userOps; IAggregator aggregator = opa.aggregator; uint256 opslen = ops.length; @@ -234,7 +234,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, for (uint256 a = 0; a < opasLen; a++) { UserOpsPerAggregator calldata opa = opsPerAggregator[a]; emit SignatureAggregatorChanged(address(opa.aggregator)); - UserOperation[] calldata ops = opa.userOps; + PackedUserOperation[] calldata ops = opa.userOps; uint256 opslen = ops.length; for (uint256 i = 0; i < opslen; i++) { @@ -254,8 +254,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, struct MemoryUserOp { address sender; uint256 nonce; - uint256 callGasLimit; - uint256 verificationGasLimit; + uint128 verificationGasLimit; + uint128 callGasLimit; + uint128 paymasterVerificationGasLimit; + uint128 paymasterPostOpGasLimit; uint256 preVerificationGas; address paymaster; uint256 maxFeePerGas; @@ -290,7 +292,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, unchecked { // handleOps was called with gas limit too low. abort entire bundle. if ( - gasleft() < callGasLimit + mUserOp.verificationGasLimit + 5000 + gasleft() < + callGasLimit + + mUserOp.paymasterPostOpGasLimit + + 5000 ) { assembly { mstore(0, INNER_OUT_OF_GAS) @@ -325,7 +330,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, /// @inheritdoc IEntryPoint function getUserOpHash( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) public view returns (bytes32) { return keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); @@ -337,25 +342,26 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, * @param mUserOp - The memory user operation. */ function _copyUserOpToMemory( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, MemoryUserOp memory mUserOp ) internal pure { mUserOp.sender = userOp.sender; mUserOp.nonce = userOp.nonce; - mUserOp.callGasLimit = userOp.callGasLimit; - mUserOp.verificationGasLimit = userOp.verificationGasLimit; + (mUserOp.verificationGasLimit, mUserOp.callGasLimit) = UserOperationLib.unpackAccountGasLimits(userOp.accountGasLimits); mUserOp.preVerificationGas = userOp.preVerificationGas; mUserOp.maxFeePerGas = userOp.maxFeePerGas; mUserOp.maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; bytes calldata paymasterAndData = userOp.paymasterAndData; if (paymasterAndData.length > 0) { require( - paymasterAndData.length >= 20, + paymasterAndData.length >= UserOperationLib.PAYMASTER_DATA_OFFSET, "AA93 invalid paymasterAndData" ); - mUserOp.paymaster = address(bytes20(paymasterAndData[:20])); + (mUserOp.paymaster, mUserOp.paymasterVerificationGasLimit, mUserOp.paymasterPostOpGasLimit) = UserOperationLib.unpackPaymasterStaticFields(paymasterAndData); } else { mUserOp.paymaster = address(0); + mUserOp.paymasterVerificationGasLimit = 0; + mUserOp.paymasterPostOpGasLimit = 0; } } @@ -367,12 +373,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, MemoryUserOp memory mUserOp ) internal pure returns (uint256 requiredPrefund) { unchecked { - // When using a Paymaster, the verificationGasLimit is used also to as a limit for the postOp call. - // Our security model might call postOp eventually twice. - uint256 mul = mUserOp.paymaster != address(0) ? 2 : 1; - uint256 requiredGas = mUserOp.callGasLimit + - mUserOp.verificationGasLimit * - mul + + uint256 requiredGas = mUserOp.verificationGasLimit + + mUserOp.callGasLimit + + mUserOp.paymasterVerificationGasLimit + + mUserOp.paymasterPostOpGasLimit + mUserOp.preVerificationGas; requiredPrefund = requiredGas * mUserOp.maxFeePerGas; @@ -430,18 +434,16 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, */ function _validateAccountPrepayment( uint256 opIndex, - UserOperation calldata op, + PackedUserOperation calldata op, UserOpInfo memory opInfo, uint256 requiredPrefund ) internal returns ( - uint256 gasUsedByValidateAccountPrepayment, uint256 validationData ) { unchecked { - uint256 preGas = gasleft(); MemoryUserOp memory mUserOp = opInfo.mUserOp; address sender = mUserOp.sender; _createSenderIfNeeded(opIndex, opInfo, op.initCode); @@ -470,7 +472,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, } senderInfo.deposit = deposit - requiredPrefund; } - gasUsedByValidateAccountPrepayment = preGas - gasleft(); } } @@ -484,25 +485,15 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, * @param op - The user operation. * @param opInfo - The operation info. * @param requiredPreFund - The required prefund amount. - * @param gasUsedByValidateAccountPrepayment - The gas used by _validateAccountPrepayment. */ function _validatePaymasterPrepayment( uint256 opIndex, - UserOperation calldata op, + PackedUserOperation calldata op, UserOpInfo memory opInfo, - uint256 requiredPreFund, - uint256 gasUsedByValidateAccountPrepayment + uint256 requiredPreFund ) internal returns (bytes memory context, uint256 validationData) { unchecked { MemoryUserOp memory mUserOp = opInfo.mUserOp; - uint256 verificationGasLimit = mUserOp.verificationGasLimit; - require( - verificationGasLimit > gasUsedByValidateAccountPrepayment, - "AA41 too little verificationGas" - ); - uint256 gas = verificationGasLimit - - gasUsedByValidateAccountPrepayment; - address paymaster = mUserOp.paymaster; DepositInfo storage paymasterInfo = deposits[paymaster]; uint256 deposit = paymasterInfo.deposit; @@ -511,7 +502,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, } paymasterInfo.deposit = deposit - requiredPreFund; try - IPaymaster(paymaster).validatePaymasterUserOp{gas: gas}( + IPaymaster(paymaster).validatePaymasterUserOp{gas: mUserOp.paymasterVerificationGasLimit}( op, opInfo.userOpHash, requiredPreFund @@ -586,7 +577,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, */ function _validatePrepayment( uint256 opIndex, - UserOperation calldata userOp, + PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo ) internal @@ -602,16 +593,14 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, uint256 maxGasValues = mUserOp.preVerificationGas | mUserOp.verificationGasLimit | mUserOp.callGasLimit | + mUserOp.paymasterVerificationGasLimit | + mUserOp.paymasterPostOpGasLimit | userOp.maxFeePerGas | userOp.maxPriorityFeePerGas; require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); - uint256 gasUsedByValidateAccountPrepayment; uint256 requiredPreFund = _getRequiredPrefund(mUserOp); - ( - gasUsedByValidateAccountPrepayment, - validationData - ) = _validateAccountPrepayment( + validationData = _validateAccountPrepayment( opIndex, userOp, outOpInfo, @@ -628,14 +617,13 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, opIndex, userOp, outOpInfo, - requiredPreFund, - gasUsedByValidateAccountPrepayment + requiredPreFund ); } unchecked { uint256 gasUsed = preGas - gasleft(); - if (userOp.verificationGasLimit < gasUsed) { + if (mUserOp.verificationGasLimit + mUserOp.paymasterVerificationGasLimit < gasUsed) { revert FailedOp(opIndex, "AA40 over verificationGasLimit"); } outOpInfo.prefund = requiredPreFund; @@ -676,7 +664,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, actualGasCost = actualGas * gasPrice; if (mode != IPaymaster.PostOpMode.postOpReverted) { try IPaymaster(paymaster).postOp{ - gas: mUserOp.verificationGasLimit + gas: mUserOp.paymasterPostOpGasLimit }(mode, context, actualGasCost) // solhint-disable-next-line no-empty-blocks {} catch { @@ -693,7 +681,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, uint256 executionGasLimit = mUserOp.callGasLimit; // Note that 'verificationGasLimit' here is the limit given to the 'postOp' which is part of execution if (context.length > 0){ - executionGasLimit += mUserOp.verificationGasLimit; + executionGasLimit += mUserOp.paymasterPostOpGasLimit; } uint256 executionGasUsed = actualGas - opInfo.preOpGas; // this check is required for the gas used within EntryPoint and not covered by explicit gas limits diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index 95f70687..d20ba78d 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -26,7 +26,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { /// @inheritdoc IEntryPointSimulations function simulateValidation( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) external returns ( @@ -85,7 +85,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { /// @inheritdoc IEntryPointSimulations function simulateHandleOp( - UserOperation calldata op, + PackedUserOperation calldata op, address target, bytes calldata targetCallData ) @@ -121,7 +121,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { } function _simulationOnlyValidations( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) internal view diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 19b678a4..511b3a55 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -3,19 +3,23 @@ pragma solidity ^0.8.12; /* solhint-disable no-inline-assembly */ -import "../interfaces/UserOperation.sol"; +import "../interfaces/PackedUserOperation.sol"; import {calldataKeccak} from "./Helpers.sol"; /** * Utility functions helpful when working with UserOperation structs. */ library UserOperationLib { + + uint256 public constant PAYMASTER_VALIDATION_GAS_OFFSET = 20; + uint256 public constant PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 public constant PAYMASTER_DATA_OFFSET = 52; /** * Get sender from user operation data. * @param userOp - The user operation data. */ function getSender( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) internal pure returns (address) { address data; //read sender from userOp, which is first userOp member (saves 800 gas...) @@ -31,7 +35,7 @@ library UserOperationLib { * @param userOp - The user operation data. */ function gasPrice( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) internal view returns (uint256) { unchecked { uint256 maxFeePerGas = userOp.maxFeePerGas; @@ -48,15 +52,14 @@ library UserOperationLib { * Pack the user operation data into bytes for hashing. * @param userOp - The user operation data. */ - function pack( - UserOperation calldata userOp + function encode( + PackedUserOperation calldata userOp ) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; bytes32 hashInitCode = calldataKeccak(userOp.initCode); bytes32 hashCallData = calldataKeccak(userOp.callData); - uint256 callGasLimit = userOp.callGasLimit; - uint256 verificationGasLimit = userOp.verificationGasLimit; + bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; uint256 maxFeePerGas = userOp.maxFeePerGas; uint256 maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; @@ -65,20 +68,36 @@ library UserOperationLib { return abi.encode( sender, nonce, hashInitCode, hashCallData, - callGasLimit, verificationGasLimit, preVerificationGas, + accountGasLimits, preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, hashPaymasterAndData ); } + function unpackAccountGasLimits( + bytes32 accountGasLimits + ) internal pure returns (uint128 validationGasLimit, uint128 callGasLimit) { + return (uint128(bytes16(accountGasLimits)), uint128(uint256(accountGasLimits))); + } + + function unpackPaymasterStaticFields( + bytes calldata paymasterAndData + ) internal pure returns (address paymaster, uint128 validationGasLimit, uint128 postOp) { + return ( + address(bytes20(paymasterAndData[: PAYMASTER_VALIDATION_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_POSTOP_GAS_OFFSET])), + uint128(bytes16(paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])) + ); + } + /** * Hash the user operation data. * @param userOp - The user operation data. */ function hash( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) internal pure returns (bytes32) { - return keccak256(pack(userOp)); + return keccak256(encode(userOp)); } /** diff --git a/contracts/interfaces/IAccount.sol b/contracts/interfaces/IAccount.sol index 6609e989..7d4f04e5 100644 --- a/contracts/interfaces/IAccount.sol +++ b/contracts/interfaces/IAccount.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; interface IAccount { /** @@ -32,7 +32,7 @@ interface IAccount { * Note that the validation code cannot use block.timestamp (or block.number) directly. */ function validateUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256 validationData); diff --git a/contracts/interfaces/IAccountExecute.sol b/contracts/interfaces/IAccountExecute.sol index 440bd4b0..825ab90e 100644 --- a/contracts/interfaces/IAccountExecute.sol +++ b/contracts/interfaces/IAccountExecute.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; interface IAccountExecute { /** @@ -14,7 +14,7 @@ interface IAccountExecute { * @param userOpHash - Hash of the user's request data. */ function executeUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash ) external; } diff --git a/contracts/interfaces/IAggregator.sol b/contracts/interfaces/IAggregator.sol index 4226a9c1..ad5efe1d 100644 --- a/contracts/interfaces/IAggregator.sol +++ b/contracts/interfaces/IAggregator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; /** * Aggregated Signatures validator. @@ -14,7 +14,7 @@ interface IAggregator { * @param signature - The aggregated signature. */ function validateSignatures( - UserOperation[] calldata userOps, + PackedUserOperation[] calldata userOps, bytes calldata signature ) external view; @@ -27,7 +27,7 @@ interface IAggregator { * (usually empty, unless account and aggregator support some kind of "multisig". */ function validateUserOpSignature( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) external view returns (bytes memory sigForUserOp); /** @@ -38,6 +38,6 @@ interface IAggregator { * @return aggregatedSignature - The aggregated signature. */ function aggregateSignatures( - UserOperation[] calldata userOps + PackedUserOperation[] calldata userOps ) external view returns (bytes memory aggregatedSignature); } diff --git a/contracts/interfaces/IEntryPoint.sol b/contracts/interfaces/IEntryPoint.sol index c815a264..01e7c483 100644 --- a/contracts/interfaces/IEntryPoint.sol +++ b/contracts/interfaces/IEntryPoint.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.12; /* solhint-disable no-inline-assembly */ /* solhint-disable reason-string */ -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; import "./IStakeManager.sol"; import "./IAggregator.sol"; import "./INonceManager.sol"; @@ -124,7 +124,7 @@ interface IEntryPoint is IStakeManager, INonceManager { // UserOps handled, per aggregator. struct UserOpsPerAggregator { - UserOperation[] userOps; + PackedUserOperation[] userOps; // Aggregator address IAggregator aggregator; // Aggregated signature @@ -140,7 +140,7 @@ interface IEntryPoint is IStakeManager, INonceManager { * @param beneficiary - The address to receive the fees. */ function handleOps( - UserOperation[] calldata ops, + PackedUserOperation[] calldata ops, address payable beneficiary ) external; @@ -160,7 +160,7 @@ interface IEntryPoint is IStakeManager, INonceManager { * @param userOp - The user operation to generate the request ID for. */ function getUserOpHash( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) external view returns (bytes32); /** diff --git a/contracts/interfaces/IEntryPointSimulations.sol b/contracts/interfaces/IEntryPointSimulations.sol index bcc822c5..9e2c5d26 100644 --- a/contracts/interfaces/IEntryPointSimulations.sol +++ b/contracts/interfaces/IEntryPointSimulations.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; import "./IEntryPoint.sol"; interface IEntryPointSimulations is IEntryPoint { @@ -41,7 +41,7 @@ interface IEntryPointSimulations is IEntryPoint { * @param userOp - The user operation to validate. */ function simulateValidation( - UserOperation calldata userOp + PackedUserOperation calldata userOp ) external returns ( @@ -62,7 +62,7 @@ interface IEntryPointSimulations is IEntryPoint { * @param targetCallData - CallData to pass to target address. */ function simulateHandleOp( - UserOperation calldata op, + PackedUserOperation calldata op, address target, bytes calldata targetCallData ) diff --git a/contracts/interfaces/IPaymaster.sol b/contracts/interfaces/IPaymaster.sol index 7a6e076e..65f69093 100644 --- a/contracts/interfaces/IPaymaster.sol +++ b/contracts/interfaces/IPaymaster.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "./UserOperation.sol"; +import "./PackedUserOperation.sol"; /** * The interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. @@ -37,7 +37,7 @@ interface IPaymaster { * Note that the validation code cannot use block.timestamp (or block.number) directly. */ function validatePaymasterUserOp( - UserOperation calldata userOp, + PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost ) external returns (bytes memory context, uint256 validationData); diff --git a/contracts/interfaces/UserOperation.sol b/contracts/interfaces/PackedUserOperation.sol similarity index 80% rename from contracts/interfaces/UserOperation.sol rename to contracts/interfaces/PackedUserOperation.sol index 8185d1c4..7250ae66 100644 --- a/contracts/interfaces/UserOperation.sol +++ b/contracts/interfaces/PackedUserOperation.sol @@ -7,23 +7,21 @@ pragma solidity ^0.8.12; * @param nonce - Unique value the sender uses to verify it is not a replay. * @param initCode - If set, the account contract will be created by this constructor/ * @param callData - The method call to execute on this account. - * @param callGasLimit - The gas limit passed to the callData method call. - * @param verificationGasLimit - Gas used for validateUserOp and validatePaymasterUserOp. + * @param accountGasLimits - Packed gas limits for validateUserOp and gas limit passed to the callData method call. * @param preVerificationGas - Gas not calculated by the handleOps method, but added to the gas paid. * Covers batch overhead. * @param maxFeePerGas - Same as EIP-1559 gas parameter. * @param maxPriorityFeePerGas - Same as EIP-1559 gas parameter. - * @param paymasterAndData - If set, this field holds the paymaster address and paymaster-specific data. + * @param paymasterAndData - If set, this field holds the paymaster address, verification gas limit, postOp gas limit and paymaster-specific extra data * The paymaster will pay for the transaction instead of the sender. * @param signature - Sender-verified signature over the entire request, the EntryPoint address and the chain ID. */ -struct UserOperation { +struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; - uint256 callGasLimit; - uint256 verificationGasLimit; + bytes32 accountGasLimits; uint256 preVerificationGas; uint256 maxFeePerGas; uint256 maxPriorityFeePerGas; diff --git a/contracts/samples/LegacyTokenPaymaster.sol b/contracts/samples/LegacyTokenPaymaster.sol index d9ebf9bc..d673aefc 100644 --- a/contracts/samples/LegacyTokenPaymaster.sol +++ b/contracts/samples/LegacyTokenPaymaster.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../core/BasePaymaster.sol"; +import "../core/UserOperationLib.sol"; /** * A sample paymaster that defines itself as a token to pay for gas. @@ -69,13 +70,13 @@ contract LegacyTokenPaymaster is BasePaymaster, ERC20 { * verify the sender has enough tokens. * (since the paymaster is also the token, there is no notion of "approval") */ - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) internal view override returns (bytes memory context, uint256 validationData) { uint256 tokenPrefund = getTokenValueOfEth(requiredPreFund); // verificationGasLimit is dual-purposed, as gas limit for postOp. make sure it is high enough // make sure that verificationGasLimit is high enough to handle postOp - require(userOp.verificationGasLimit > COST_OF_POST, "TokenPaymaster: gas too low for postOp"); + require(uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])) > COST_OF_POST, "TokenPaymaster: gas too low for postOp"); if (userOp.initCode.length != 0) { _validateConstructor(userOp); @@ -90,7 +91,7 @@ contract LegacyTokenPaymaster is BasePaymaster, ERC20 { // when constructing an account, validate constructor code and parameters // we trust our factory (and that it doesn't have any other public methods) - function _validateConstructor(UserOperation calldata userOp) internal virtual view { + function _validateConstructor(PackedUserOperation calldata userOp) internal virtual view { address factory = address(bytes20(userOp.initCode[0 : 20])); require(factory == theFactory, "TokenPaymaster: wrong account factory"); } diff --git a/contracts/samples/SimpleAccount.sol b/contracts/samples/SimpleAccount.sol index ecdb19e0..412b9a68 100644 --- a/contracts/samples/SimpleAccount.sol +++ b/contracts/samples/SimpleAccount.sol @@ -94,7 +94,7 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In } /// implement template method of BaseAccount - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override virtual returns (uint256 validationData) { bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); if (owner != ECDSA.recover(hash, userOp.signature)) diff --git a/contracts/samples/TokenPaymaster.sol b/contracts/samples/TokenPaymaster.sol index f5cfac58..00238e1e 100644 --- a/contracts/samples/TokenPaymaster.sol +++ b/contracts/samples/TokenPaymaster.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "../interfaces/IEntryPoint.sol"; import "../core/BasePaymaster.sol"; +import "../core/UserOperationLib.sol"; import "./utils/UniswapHelper.sol"; import "./utils/OracleHelper.sol"; @@ -115,12 +116,12 @@ contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { /// @param requiredPreFund The amount of tokens required for pre-funding. /// @return context The context containing the token amount and user sender address (if applicable). /// @return validationResult A uint256 value indicating the result of the validation (always 0 in this implementation). - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 requiredPreFund) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256 requiredPreFund) internal override returns (bytes memory context, uint256 validationResult) {unchecked { uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; - uint256 paymasterAndDataLength = userOp.paymasterAndData.length - 20; + uint256 paymasterAndDataLength = userOp.paymasterAndData.length - PAYMASTER_DATA_OFFSET; require(paymasterAndDataLength == 0 || paymasterAndDataLength == 32, "TPM: invalid data length" ); @@ -128,7 +129,7 @@ contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { // note: as price is in ether-per-token and we want more tokens increasing it means dividing it by markup uint256 cachedPriceWithMarkup = cachedPrice * PRICE_DENOMINATOR / priceMarkup; if (paymasterAndDataLength == 32) { - uint256 clientSuppliedPrice = uint256(bytes32(userOp.paymasterAndData[20 : 52])); + uint256 clientSuppliedPrice = uint256(bytes32(userOp.paymasterAndData[PAYMASTER_DATA_OFFSET : PAYMASTER_DATA_OFFSET + 32])); if (clientSuppliedPrice < cachedPriceWithMarkup) { // note: smaller number means 'more ether per token' cachedPriceWithMarkup = clientSuppliedPrice; diff --git a/contracts/samples/VerifyingPaymaster.sol b/contracts/samples/VerifyingPaymaster.sol index 33036f6e..6fb13ccc 100644 --- a/contracts/samples/VerifyingPaymaster.sol +++ b/contracts/samples/VerifyingPaymaster.sol @@ -19,19 +19,18 @@ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; */ contract VerifyingPaymaster is BasePaymaster { - using UserOperationLib for UserOperation; + using UserOperationLib for PackedUserOperation; address public immutable verifyingSigner; - uint256 private constant VALID_TIMESTAMP_OFFSET = 20; + uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; - uint256 private constant SIGNATURE_OFFSET = 84; + uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { verifyingSigner = _verifyingSigner; } - /** * return the hash we're going to sign off-chain (and validate on-chain) * this method is called by the off-chain service, to sign the request. @@ -39,28 +38,28 @@ contract VerifyingPaymaster is BasePaymaster { * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", * which will carry the signature itself. */ - function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter) + function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter) public view returns (bytes32) { //can't use userOp.hash(), since it contains also the paymasterAndData itself. address sender = userOp.getSender(); return keccak256( - abi.encode( - sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.callGasLimit, - userOp.verificationGasLimit, - userOp.preVerificationGas, - userOp.maxFeePerGas, - userOp.maxPriorityFeePerGas, - block.chainid, - address(this), - validUntil, - validAfter - ) - ); + abi.encode( + sender, + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.accountGasLimits, + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas, + block.chainid, + address(this), + validUntil, + validAfter + ) + ); } /** @@ -70,7 +69,7 @@ contract VerifyingPaymaster is BasePaymaster { * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) * paymasterAndData[84:] : signature */ - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) internal view override returns (bytes memory context, uint256 validationData) { (requiredPreFund); @@ -82,16 +81,16 @@ contract VerifyingPaymaster is BasePaymaster { //don't revert on signature failure: return SIG_VALIDATION_FAILED if (verifyingSigner != ECDSA.recover(hash, signature)) { - return ("",_packValidationData(true,validUntil,validAfter)); + return ("", _packValidationData(true, validUntil, validAfter)); } //no need for other on-chain validation: entire UserOp should have been checked // by the external service prior to signing it. - return ("",_packValidationData(false,validUntil,validAfter)); + return ("", _packValidationData(false, validUntil, validAfter)); } - function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns(uint48 validUntil, uint48 validAfter, bytes calldata signature) { - (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],(uint48, uint48)); - signature = paymasterAndData[SIGNATURE_OFFSET:]; + function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) { + (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET :], (uint48, uint48)); + signature = paymasterAndData[SIGNATURE_OFFSET :]; } } diff --git a/contracts/samples/bls/BLSAccount.sol b/contracts/samples/bls/BLSAccount.sol index efc64ce1..82954a05 100644 --- a/contracts/samples/bls/BLSAccount.sol +++ b/contracts/samples/bls/BLSAccount.sol @@ -30,7 +30,7 @@ contract BLSAccount is SimpleAccount, IBLSAccount { _setBlsPublicKey(aPublicKey); } - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override view returns (uint256 validationData) { (userOp, userOpHash); diff --git a/contracts/samples/bls/BLSSignatureAggregator.sol b/contracts/samples/bls/BLSSignatureAggregator.sol index 1866692f..45084924 100644 --- a/contracts/samples/bls/BLSSignatureAggregator.sol +++ b/contracts/samples/bls/BLSSignatureAggregator.sol @@ -13,7 +13,7 @@ import "./BLSHelper.sol"; * A BLS-based signature aggregator, to validate aggregated signature of multiple UserOps if BLSAccount */ contract BLSSignatureAggregator is IAggregator { - using UserOperationLib for UserOperation; + using UserOperationLib for PackedUserOperation; bytes32 public constant BLS_DOMAIN = keccak256("eip4337.bls.domain"); @@ -31,7 +31,7 @@ contract BLSSignatureAggregator is IAggregator { * normally public key will be queried from the deployed BLSAccount itself; * the public key will be read from the 'initCode' if the account is not deployed yet; */ - function getUserOpPublicKey(UserOperation memory userOp) public view returns (uint256[4] memory publicKey) { + function getUserOpPublicKey(PackedUserOperation memory userOp) public view returns (uint256[4] memory publicKey) { bytes memory initCode = userOp.initCode; if (initCode.length > 0) { publicKey = getTrailingPublicKey(initCode); @@ -59,7 +59,7 @@ contract BLSSignatureAggregator is IAggregator { } /// @inheritdoc IAggregator - function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view override { require(signature.length == 64, "BLS: invalid signature"); (uint256[2] memory blsSignature) = abi.decode(signature, (uint256[2])); @@ -69,7 +69,7 @@ contract BLSSignatureAggregator is IAggregator { uint256[2][] memory messages = new uint256[2][](userOpsLen); for (uint256 i = 0; i < userOpsLen; i++) { - UserOperation memory userOp = userOps[i]; + PackedUserOperation memory userOp = userOps[i]; blsPublicKeys[i] = getUserOpPublicKey(userOp); messages[i] = _userOpToMessage(userOp, _getPublicKeyHash(blsPublicKeys[i])); @@ -82,14 +82,13 @@ contract BLSSignatureAggregator is IAggregator { * NOTE: this hash is not the same as UserOperation.hash() * (slightly less efficient, since it uses memory userOp) */ - function internalUserOpHash(UserOperation memory userOp) internal pure returns (bytes32) { + function internalUserOpHash(PackedUserOperation memory userOp) internal pure returns (bytes32) { return keccak256(abi.encode( userOp.sender, userOp.nonce, keccak256(userOp.initCode), keccak256(userOp.callData), - userOp.callGasLimit, - userOp.verificationGasLimit, + userOp.accountGasLimits, userOp.preVerificationGas, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, @@ -101,22 +100,22 @@ contract BLSSignatureAggregator is IAggregator { * return the BLS "message" for the given UserOp. * the account checks the signature over this value using its public key */ - function userOpToMessage(UserOperation memory userOp) public view returns (uint256[2] memory) { + function userOpToMessage(PackedUserOperation memory userOp) public view returns (uint256[2] memory) { bytes32 publicKeyHash = _getPublicKeyHash(getUserOpPublicKey(userOp)); return _userOpToMessage(userOp, publicKeyHash); } - function _userOpToMessage(UserOperation memory userOp, bytes32 publicKeyHash) internal view returns (uint256[2] memory) { + function _userOpToMessage(PackedUserOperation memory userOp, bytes32 publicKeyHash) internal view returns (uint256[2] memory) { bytes32 userOpHash = _getUserOpHash(userOp, publicKeyHash); return BLSOpen.hashToPoint(BLS_DOMAIN, abi.encodePacked(userOpHash)); } - function getUserOpHash(UserOperation memory userOp) public view returns (bytes32) { + function getUserOpHash(PackedUserOperation memory userOp) public view returns (bytes32) { bytes32 publicKeyHash = _getPublicKeyHash(getUserOpPublicKey(userOp)); return _getUserOpHash(userOp, publicKeyHash); } - function _getUserOpHash(UserOperation memory userOp, bytes32 publicKeyHash) internal view returns (bytes32) { + function _getUserOpHash(PackedUserOperation memory userOp, bytes32 publicKeyHash) internal view returns (bytes32) { return keccak256(abi.encode(internalUserOpHash(userOp), publicKeyHash, address(this), block.chainid, entryPoint)); } @@ -131,7 +130,7 @@ contract BLSSignatureAggregator is IAggregator { * @return sigForUserOp the value to put into the signature field of the userOp when calling handleOps. * (usually empty, unless account and aggregator support some kind of "multisig" */ - function validateUserOpSignature(UserOperation calldata userOp) + function validateUserOpSignature(PackedUserOperation calldata userOp) external view returns (bytes memory sigForUserOp) { uint256[2] memory signature = abi.decode(userOp.signature, (uint256[2])); uint256[4] memory pubkey = getUserOpPublicKey(userOp); @@ -149,7 +148,7 @@ contract BLSSignatureAggregator is IAggregator { * @param userOps array of UserOperations to collect the signatures from. * @return aggregatedSignature the aggregated signature */ - function aggregateSignatures(UserOperation[] calldata userOps) external pure returns (bytes memory aggregatedSignature) { + function aggregateSignatures(PackedUserOperation[] calldata userOps) external pure returns (bytes memory aggregatedSignature) { BLSHelper.XY[] memory points = new BLSHelper.XY[](userOps.length); for (uint i = 0; i < points.length; i++) { (uint256 x, uint256 y) = abi.decode(userOps[i].signature, (uint256, uint256)); diff --git a/contracts/test/BrokenBlsAccount.sol b/contracts/test/BrokenBlsAccount.sol index 5690c696..dfd7381f 100644 --- a/contracts/test/BrokenBlsAccount.sol +++ b/contracts/test/BrokenBlsAccount.sol @@ -27,7 +27,7 @@ contract BrokenBLSAccount is SimpleAccount, IBLSAccount { super._initialize(address(0)); } - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override view returns (uint256 validationData) { (userOp, userOpHash); diff --git a/contracts/test/MaliciousAccount.sol b/contracts/test/MaliciousAccount.sol index 57325ddc..704e44f9 100644 --- a/contracts/test/MaliciousAccount.sol +++ b/contracts/test/MaliciousAccount.sol @@ -2,18 +2,22 @@ pragma solidity ^0.8.12; import "../interfaces/IAccount.sol"; import "../interfaces/IEntryPoint.sol"; +import "../core/UserOperationLib.sol"; contract MaliciousAccount is IAccount { IEntryPoint private ep; constructor(IEntryPoint _ep) payable { ep = _ep; } - function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds) + function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingAccountFunds) external returns (uint256 validationData) { ep.depositTo{value : missingAccountFunds}(address(this)); // Now calculate basefee per EntryPoint.getUserOpGasPrice() and compare it to the basefe we pass off-chain in the signature uint256 externalBaseFee = abi.decode(userOp.signature, (uint256)); - uint256 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas; + (uint128 verificationGasLimit, uint128 callGasLimit) = UserOperationLib.unpackAccountGasLimits(userOp.accountGasLimits); + uint256 requiredGas = verificationGasLimit + + callGasLimit + + userOp.preVerificationGas; uint256 gasPrice = missingAccountFunds / requiredGas; uint256 basefee = gasPrice - userOp.maxPriorityFeePerGas; require (basefee == externalBaseFee, "Revert after first validation"); diff --git a/contracts/test/TestAggregatedAccount.sol b/contracts/test/TestAggregatedAccount.sol index 23bf2954..1d5d93b1 100644 --- a/contracts/test/TestAggregatedAccount.sol +++ b/contracts/test/TestAggregatedAccount.sol @@ -22,7 +22,7 @@ contract TestAggregatedAccount is SimpleAccount { super._initialize(address(0)); } - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override view returns (uint256 validationData) { (userOp, userOpHash); return _packValidationData(ValidationData(aggregator, 0, 0)); diff --git a/contracts/test/TestExecAccount.sol b/contracts/test/TestExecAccount.sol index 9a624f03..44df4040 100644 --- a/contracts/test/TestExecAccount.sol +++ b/contracts/test/TestExecAccount.sol @@ -20,9 +20,9 @@ contract TestExecAccount is SimpleAccount, IAccountExecute { constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint){ } - event Executed(UserOperation userOp, bytes innerCallRet); + event Executed(PackedUserOperation userOp, bytes innerCallRet); - function executeUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/) external { + function executeUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/) external { _requireFromEntryPointOrOwner(); // read from the userOp.callData, but skip the "magic" prefix (executeUserOp sig), diff --git a/contracts/test/TestExpirePaymaster.sol b/contracts/test/TestExpirePaymaster.sol index 51036a1c..a92e3ff2 100644 --- a/contracts/test/TestExpirePaymaster.sol +++ b/contracts/test/TestExpirePaymaster.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.12; import "../core/BasePaymaster.sol"; +import "../core/UserOperationLib.sol"; /** * test expiry mechanism: paymasterData encodes the "validUntil" and validAfter" times @@ -11,11 +12,11 @@ contract TestExpirePaymaster is BasePaymaster { constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {} - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) internal virtual override view returns (bytes memory context, uint256 validationData) { (userOp, userOpHash, maxCost); - (uint48 validAfter, uint48 validUntil) = abi.decode(userOp.paymasterAndData[20 :], (uint48, uint48)); + (uint48 validAfter, uint48 validUntil) = abi.decode(userOp.paymasterAndData[PAYMASTER_DATA_OFFSET :], (uint48, uint48)); validationData = _packValidationData(false, validUntil, validAfter); context = ""; } diff --git a/contracts/test/TestExpiryAccount.sol b/contracts/test/TestExpiryAccount.sol index b613397e..0bbad359 100644 --- a/contracts/test/TestExpiryAccount.sol +++ b/contracts/test/TestExpiryAccount.sol @@ -34,7 +34,7 @@ contract TestExpiryAccount is SimpleAccount { } /// implement template method of BaseAccount - function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override view returns (uint256 validationData) { bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); address signer = ECDSA.recover(hash,userOp.signature); diff --git a/contracts/test/TestPaymasterAcceptAll.sol b/contracts/test/TestPaymasterAcceptAll.sol index 7db7dc2c..716035d7 100644 --- a/contracts/test/TestPaymasterAcceptAll.sol +++ b/contracts/test/TestPaymasterAcceptAll.sol @@ -17,7 +17,7 @@ contract TestPaymasterAcceptAll is BasePaymaster { } - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) internal virtual override view returns (bytes memory context, uint256 validationData) { (userOp, userOpHash, maxCost); diff --git a/contracts/test/TestPaymasterRevertCustomError.sol b/contracts/test/TestPaymasterRevertCustomError.sol index b0be4b00..6af71bf4 100644 --- a/contracts/test/TestPaymasterRevertCustomError.sol +++ b/contracts/test/TestPaymasterRevertCustomError.sol @@ -22,7 +22,7 @@ contract TestPaymasterRevertCustomError is BasePaymaster { constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {} - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256) internal virtual override view returns (bytes memory context, uint256 validationData) { validationData = 0; diff --git a/contracts/test/TestPaymasterWithPostOp.sol b/contracts/test/TestPaymasterWithPostOp.sol index 694dde51..9590d513 100644 --- a/contracts/test/TestPaymasterWithPostOp.sol +++ b/contracts/test/TestPaymasterWithPostOp.sol @@ -13,7 +13,7 @@ contract TestPaymasterWithPostOp is TestPaymasterAcceptAll { constructor(IEntryPoint _entryPoint) TestPaymasterAcceptAll(_entryPoint) { } - function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) internal virtual override view returns (bytes memory context, uint256 validationData) { (userOp, userOpHash, maxCost); diff --git a/contracts/test/TestRevertAccount.sol b/contracts/test/TestRevertAccount.sol index d7c376c4..6b0523de 100644 --- a/contracts/test/TestRevertAccount.sol +++ b/contracts/test/TestRevertAccount.sol @@ -9,7 +9,7 @@ contract TestRevertAccount is IAccount { ep = _ep; } - function validateUserOp(UserOperation calldata, bytes32, uint256 missingAccountFunds) + function validateUserOp(PackedUserOperation calldata, bytes32, uint256 missingAccountFunds) external override returns (uint256 validationData) { ep.depositTo{value : missingAccountFunds}(address(this)); return 0; diff --git a/contracts/test/TestSignatureAggregator.sol b/contracts/test/TestSignatureAggregator.sol index 97b9eb5f..1eeb68fd 100644 --- a/contracts/test/TestSignatureAggregator.sol +++ b/contracts/test/TestSignatureAggregator.sol @@ -14,7 +14,7 @@ import "../samples/SimpleAccount.sol"; contract TestSignatureAggregator is IAggregator { /// @inheritdoc IAggregator - function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) external pure override { + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external pure override { uint sum = 0; for (uint i = 0; i < userOps.length; i++) { uint nonce = userOps[i].nonce; @@ -26,7 +26,7 @@ contract TestSignatureAggregator is IAggregator { } /// @inheritdoc IAggregator - function validateUserOpSignature(UserOperation calldata) + function validateUserOpSignature(PackedUserOperation calldata) external pure returns (bytes memory) { return ""; } @@ -34,7 +34,7 @@ contract TestSignatureAggregator is IAggregator { /** * dummy test aggregator: sum all nonce values of UserOps. */ - function aggregateSignatures(UserOperation[] calldata userOps) external pure returns (bytes memory aggregatedSignature) { + function aggregateSignatures(PackedUserOperation[] calldata userOps) external pure returns (bytes memory aggregatedSignature) { uint sum = 0; for (uint i = 0; i < userOps.length; i++) { sum += userOps[i].nonce; diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol index 4257c3fb..a1e10f53 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; -import "../interfaces/UserOperation.sol"; +import "../interfaces/PackedUserOperation.sol"; import "../core/UserOperationLib.sol"; contract TestUtil { - using UserOperationLib for UserOperation; + using UserOperationLib for PackedUserOperation; - function packUserOp(UserOperation calldata op) external pure returns (bytes memory){ - return op.pack(); + function encodeUserOp(PackedUserOperation calldata op) external pure returns (bytes memory){ + return op.encode(); } } diff --git a/contracts/test/TestWarmColdAccount.sol b/contracts/test/TestWarmColdAccount.sol index 76f35bc8..4e69cd6f 100644 --- a/contracts/test/TestWarmColdAccount.sol +++ b/contracts/test/TestWarmColdAccount.sol @@ -14,7 +14,7 @@ contract TestWarmColdAccount is IAccount { ep = _ep; } - function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds) + function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingAccountFunds) external override returns (uint256 validationData) { ep.depositTo{value : missingAccountFunds}(address(this)); if (userOp.nonce == 1) { diff --git a/gascalc/GasChecker.ts b/gascalc/GasChecker.ts index 1186bf38..4a2674fa 100644 --- a/gascalc/GasChecker.ts +++ b/gascalc/GasChecker.ts @@ -14,13 +14,13 @@ import { } from '../typechain' import { BigNumberish, Wallet } from 'ethers' import hre from 'hardhat' -import { fillAndSign, fillUserOp, signUserOp } from '../test/UserOp' +import { fillSignAndPack, fillUserOp, packUserOp, signUserOp } from '../test/UserOp' import { TransactionReceipt } from '@ethersproject/abstract-provider' import { table, TableUserConfig } from 'table' import { Create2Factory } from '../src/Create2Factory' import * as fs from 'fs' import { SimpleAccountInterface } from '../typechain/contracts/samples/SimpleAccount' -import { UserOperation } from '../test/UserOperation' +import { PackedUserOperation } from '../test/UserOperation' import { expect } from 'chai' const gasCheckerLogFile = './reports/gas-checker.txt' @@ -131,7 +131,7 @@ export class GasChecker { console.log('factaddr', factoryAddress) const fact = SimpleAccountFactory__factory.connect(factoryAddress, ethersSigner) // create accounts - const creationOps: UserOperation[] = [] + const creationOps: PackedUserOperation[] = [] for (const n of range(count)) { const salt = n // const initCode = this.accountInitCode(fact, salt) @@ -150,7 +150,7 @@ export class GasChecker { preVerificationGas: 1, maxFeePerGas: 0 }), this.accountOwner, this.entryPoint().address, await provider.getNetwork().then(net => net.chainId)) - creationOps.push(op) + creationOps.push(packUserOp(op)) this.createdAccounts.add(addr) } @@ -224,14 +224,16 @@ export class GasChecker { } // console.debug('== account est=', accountEst.toString()) accountEst = est.accountEst - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account, callData: accountExecFromEntryPoint, maxPriorityFeePerGas: info.gasPrice, maxFeePerGas: info.gasPrice, callGasLimit: accountEst, verificationGasLimit: 1000000, - paymasterAndData: paymaster, + paymaster: paymaster, + paymasterVerificationGasLimit: 30000, + paymasterPostOpGasLimit: 30000, preVerificationGas: 1 }, accountOwner, GasCheckCollector.inst.entryPoint) // const packed = packUserOp(op, false) diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 3fbf000b..e09f02d9 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 81570 │ │ ║ +║ simple │ 1 │ 82677 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 43842 │ 14863 ║ +║ simple - diff from previous │ 2 │ │ 44884 │ 15905 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 476227 │ │ ║ +║ simple │ 10 │ 486740 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 43873 │ 14894 ║ +║ simple - diff from previous │ 11 │ │ 44929 │ 15950 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 87750 │ │ ║ +║ simple paymaster │ 1 │ 89079 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 42716 │ 13737 ║ +║ simple paymaster with diff │ 2 │ │ 43987 │ 15008 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 472423 │ │ ║ +║ simple paymaster │ 10 │ 485083 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 42781 │ 13802 ║ +║ simple paymaster with diff │ 11 │ │ 43981 │ 15002 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 182627 │ │ ║ +║ big tx 5k │ 1 │ 183735 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 144341 │ 19117 ║ +║ big tx - diff from previous │ 2 │ │ 145383 │ 20159 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1481819 │ │ ║ +║ big tx 5k │ 10 │ 1492351 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 144445 │ 19221 ║ +║ big tx - diff from previous │ 11 │ │ 145452 │ 20228 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 89483 │ │ ║ +║ paymaster+postOp │ 1 │ 90919 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 44463 │ 15484 ║ +║ paymaster+postOp with diff │ 2 │ │ 45802 │ 16823 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 489783 │ │ ║ +║ paymaster+postOp │ 10 │ 503479 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 44508 │ 15529 ║ +║ paymaster+postOp with diff │ 11 │ │ 45875 │ 16896 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 147976 │ │ ║ +║ token paymaster │ 1 │ 149447 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 72659 │ 43680 ║ +║ token paymaster with diff │ 2 │ │ 74046 │ 45067 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 802242 │ │ ║ +║ token paymaster │ 10 │ 816174 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 72727 │ 43748 ║ +║ token paymaster with diff │ 11 │ │ 74106 │ 45127 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/src/AASigner.ts b/src/AASigner.ts index bdf1f1b6..598a598e 100644 --- a/src/AASigner.ts +++ b/src/AASigner.ts @@ -6,8 +6,8 @@ import { BaseProvider, Provider, TransactionRequest } from '@ethersproject/provi import { BigNumber, Bytes, ethers, Event, Signer } from 'ethers' import { clearInterval } from 'timers' import { getAccountAddress, getAccountInitCode } from '../test/testutils' -import { fillAndSign, getUserOpHash } from '../test/UserOp' -import { UserOperation } from '../test/UserOperation' +import { fillAndSign, getUserOpHash, packUserOp } from '../test/UserOp' +import { PackedUserOperation, UserOperation } from '../test/UserOperation' import { EntryPoint, EntryPoint__factory, @@ -132,12 +132,12 @@ async function sendQueuedUserOps (queueSender: QueueSendUserOp, entryPoint: Entr console.log('queue too small/too young. waiting') return } - const ops: UserOperation[] = [] + const ops: PackedUserOperation[] = [] const queue = queueSender.queue Object.keys(queue).forEach(sender => { const op = queue[sender].shift() if (op != null) { - ops.push(op) + ops.push(packUserOp(op)) queueSender.queueSize-- } }) @@ -174,7 +174,7 @@ export function localUserOpSender (entryPointAddress: string, signer: Signer, be } const gasLimit = BigNumber.from(userOp.preVerificationGas).add(userOp.verificationGasLimit).add(userOp.callGasLimit) console.log('calc gaslimit=', gasLimit.toString()) - const ret = await entryPoint.handleOps([userOp], beneficiary ?? await signer.getAddress(), { + const ret = await entryPoint.handleOps([packUserOp(userOp)], beneficiary ?? await signer.getAddress(), { maxPriorityFeePerGas: userOp.maxPriorityFeePerGas, maxFeePerGas: userOp.maxFeePerGas }) diff --git a/test/UserOp.ts b/test/UserOp.ts index daa0decd..fac8540a 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -5,12 +5,12 @@ import { keccak256 } from 'ethers/lib/utils' import { BigNumber, Contract, Signer, Wallet } from 'ethers' -import { AddressZero, callDataCost, rethrow } from './testutils' +import { AddressZero, callDataCost, packAccountGasLimits, packPaymasterData, rethrow } from './testutils' import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' import { EntryPoint, EntryPointSimulations__factory } from '../typechain' -import { UserOperation } from './UserOperation' +import { PackedUserOperation, UserOperation } from './UserOperation' import { Create2Factory } from '../src/Create2Factory' import { TransactionRequest } from '@ethersproject/abstract-provider' @@ -18,55 +18,49 @@ import EntryPointSimulationsJson from '../artifacts/contracts/core/EntryPointSim import { ethers } from 'hardhat' import { IEntryPointSimulations } from '../typechain/contracts/core/EntryPointSimulations' -export function packUserOp (op: UserOperation, forSignature = true): string { +export function packUserOp (userOp: UserOperation): PackedUserOperation { + const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) + let paymasterAndData = '0x' + if (userOp.paymaster.length >= 20 && userOp.paymaster !== AddressZero) { + paymasterAndData = packPaymasterData(userOp.paymaster as string, userOp.paymasterVerificationGasLimit, userOp.paymasterPostOpGasLimit, userOp.paymasterData as string) + } + return { + sender: userOp.sender, + nonce: userOp.nonce, + callData: userOp.callData, + accountGasLimits, + initCode: userOp.initCode, + preVerificationGas: userOp.preVerificationGas, + maxFeePerGas: userOp.maxFeePerGas, + maxPriorityFeePerGas: userOp.maxPriorityFeePerGas, + paymasterAndData, + signature: userOp.signature + } +} +export function encodeUserOp (userOp: UserOperation, forSignature = true): string { + const packedUserOp = packUserOp(userOp) if (forSignature) { return defaultAbiCoder.encode( ['address', 'uint256', 'bytes32', 'bytes32', - 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', + 'bytes32', 'uint256', 'uint256', 'uint256', 'bytes32'], - [op.sender, op.nonce, keccak256(op.initCode), keccak256(op.callData), - op.callGasLimit, op.verificationGasLimit, op.preVerificationGas, op.maxFeePerGas, op.maxPriorityFeePerGas, - keccak256(op.paymasterAndData)]) + [packedUserOp.sender, packedUserOp.nonce, keccak256(packedUserOp.initCode), keccak256(packedUserOp.callData), + packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.maxFeePerGas, packedUserOp.maxPriorityFeePerGas, + keccak256(packedUserOp.paymasterAndData)]) } else { // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) return defaultAbiCoder.encode( ['address', 'uint256', 'bytes', 'bytes', - 'uint256', 'uint256', 'uint256', 'uint256', 'uint256', + 'bytes32', 'uint256', 'uint256', 'uint256', 'bytes', 'bytes'], - [op.sender, op.nonce, op.initCode, op.callData, - op.callGasLimit, op.verificationGasLimit, op.preVerificationGas, op.maxFeePerGas, op.maxPriorityFeePerGas, - op.paymasterAndData, op.signature]) + [packedUserOp.sender, packedUserOp.nonce, packedUserOp.initCode, packedUserOp.callData, + packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.maxFeePerGas, packedUserOp.maxPriorityFeePerGas, + packedUserOp.paymasterAndData, packedUserOp.signature]) } } -export function packUserOp1 (op: UserOperation): string { - return defaultAbiCoder.encode([ - 'address', // sender - 'uint256', // nonce - 'bytes32', // initCode - 'bytes32', // callData - 'uint256', // callGasLimit - 'uint256', // verificationGasLimit - 'uint256', // preVerificationGas - 'uint256', // maxFeePerGas - 'uint256', // maxPriorityFeePerGas - 'bytes32' // paymasterAndData - ], [ - op.sender, - op.nonce, - keccak256(op.initCode), - keccak256(op.callData), - op.callGasLimit, - op.verificationGasLimit, - op.preVerificationGas, - op.maxFeePerGas, - op.maxPriorityFeePerGas, - keccak256(op.paymasterAndData) - ]) -} - export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { - const userOpHash = keccak256(packUserOp(op, true)) + const userOpHash = keccak256(encodeUserOp(op, true)) const enc = defaultAbiCoder.encode( ['bytes32', 'address', 'uint256'], [userOpHash, entryPoint, chainId]) @@ -83,7 +77,10 @@ export const DefaultsForUserOp: UserOperation = { preVerificationGas: 21000, // should also cover calldata cost. maxFeePerGas: 0, maxPriorityFeePerGas: 1e9, - paymasterAndData: '0x', + paymaster: AddressZero, + paymasterData: '0x', + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 0, signature: '0x' } @@ -177,6 +174,14 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry // estimateGas assumes direct call from entryPoint. add wrapper cost. op1.callGasLimit = gasEtimated // .add(55000) } + if (op1.paymaster != null) { + if (op1.paymasterVerificationGasLimit == null) { + op1.paymasterVerificationGasLimit = DefaultsForUserOp.paymasterVerificationGasLimit + } + if (op1.paymasterPostOpGasLimit == null) { + op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit + } + } if (op1.maxFeePerGas == null) { if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') const block = await provider.getBlock('latest') @@ -191,11 +196,15 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry // eslint-disable-next-line @typescript-eslint/no-base-to-string if (op2.preVerificationGas.toString() === '0') { // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. - op2.preVerificationGas = callDataCost(packUserOp(op2, false)) + op2.preVerificationGas = callDataCost(encodeUserOp(op2, false)) } return op2 } +export async function fillAndPack (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + return packUserOp(await fillUserOp(op, entryPoint, getNonceFunction)) +} + export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { const provider = entryPoint?.provider const op2 = await fillUserOp(op, entryPoint, getNonceFunction) @@ -216,6 +225,11 @@ export async function fillAndSign (op: Partial, signer: Wallet | } } +export async function fillSignAndPack (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + const filledAndSignedOp = await fillAndSign(op, signer, entryPoint, getNonceFunction) + return packUserOp(filledAndSignedOp) +} + /** * This function relies on a "state override" functionality of the 'eth_call' RPC method * in order to provide the details of a simulated validation call to the bundler @@ -224,7 +238,7 @@ export async function fillAndSign (op: Partial, signer: Wallet | * @param txOverrides */ export async function simulateValidation ( - userOp: UserOperation, + userOp: PackedUserOperation, entryPointAddress: string, txOverrides?: any): Promise { const entryPointSimulations = EntryPointSimulations__factory.createInterface() @@ -257,7 +271,7 @@ export async function simulateValidation ( // TODO: this code is very much duplicated but "encodeFunctionData" is based on 20 overloads // TypeScript is not able to resolve overloads with variables: https://github.com/microsoft/TypeScript/issues/14107 export async function simulateHandleOp ( - userOp: UserOperation, + userOp: PackedUserOperation, target: string, targetCallData: string, entryPointAddress: string, diff --git a/test/UserOperation.ts b/test/UserOperation.ts index 8bed5ab3..778f69b3 100644 --- a/test/UserOperation.ts +++ b/test/UserOperation.ts @@ -6,8 +6,25 @@ export interface UserOperation { nonce: typ.uint256 initCode: typ.bytes callData: typ.bytes - callGasLimit: typ.uint256 - verificationGasLimit: typ.uint256 + callGasLimit: typ.uint128 + verificationGasLimit: typ.uint128 + preVerificationGas: typ.uint256 + maxFeePerGas: typ.uint256 + maxPriorityFeePerGas: typ.uint256 + paymaster: typ.address + paymasterVerificationGasLimit: typ.uint128 + paymasterPostOpGasLimit: typ.uint128 + paymasterData: typ.bytes + signature: typ.bytes +} + +export interface PackedUserOperation { + + sender: typ.address + nonce: typ.uint256 + initCode: typ.bytes + callData: typ.bytes + accountGasLimits: typ.bytes32 preVerificationGas: typ.uint256 maxFeePerGas: typ.uint256 maxPriorityFeePerGas: typ.uint256 diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index ebc3acee..2bf18a03 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -47,11 +47,11 @@ import { getAggregatedAccountInitCode, decodeRevertReason } from './testutils' -import { DefaultsForUserOp, fillAndSign, getUserOpHash, simulateValidation } from './UserOp' -import { UserOperation } from './UserOperation' +import { DefaultsForUserOp, fillAndSign, fillSignAndPack, getUserOpHash, packUserOp, simulateValidation } from './UserOp' +import { PackedUserOperation, UserOperation } from './UserOperation' import { PopulatedTransaction } from 'ethers/lib/ethers' import { ethers } from 'hardhat' -import { arrayify, defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' +import { arrayify, defaultAbiCoder, hexZeroPad, parseEther } from 'ethers/lib/utils' import { debugTransaction } from './debugTx' import { BytesLike } from '@ethersproject/bytes' import { toChecksumAddress } from 'ethereumjs-util' @@ -85,7 +85,8 @@ describe('EntryPoint', function () { // sanity: validate helper functions const sampleOp = await fillAndSign({ sender: account.address }, accountOwner, entryPoint) - expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(sampleOp)) + const packedOp = packUserOp(sampleOp) + expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packedOp)) }) describe('Stake Management', () => { @@ -229,7 +230,7 @@ describe('EntryPoint', function () { // note: for the actual opcode and storage rule restrictions see the reference bundler ValidationManager it('should not use banned ops during simulateValidation', async () => { - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory), sender: await getAccountAddress(accountOwner1.address, simpleAccountFactory) }, accountOwner1, entryPoint) @@ -274,14 +275,18 @@ describe('EntryPoint', function () { // we need maxFeeperGas > block.basefee + maxPriorityFeePerGas so requiredPrefund onchain is basefee + maxPriorityFeePerGas maxFeePerGas: block.baseFeePerGas.mul(3), maxPriorityFeePerGas: block.baseFeePerGas, - paymasterAndData: '0x' + paymaster: AddressZero, + paymasterData: '0x', + paymasterVerificationGasLimit: 0, + paymasterPostOpGasLimit: 0 } + const userOpPacked = packUserOp(userOp) try { - await simulateValidation(userOp, entryPoint.address, { gasLimit: 1e6 }) + await simulateValidation(userOpPacked, entryPoint.address, { gasLimit: 1e6 }) console.log('after first simulation') await ethers.provider.send('evm_mine', []) - await expect(simulateValidation(userOp, entryPoint.address, { gasLimit: 1e6 })) + await expect(simulateValidation(userOpPacked, entryPoint.address, { gasLimit: 1e6 })) .to.revertedWith('Revert after first validation') // if we get here, it means the userOp passed first sim and reverted second expect.fail(null, null, 'should fail on first simulation') @@ -305,9 +310,10 @@ describe('EntryPoint', function () { callData: badData.data! } const beneficiaryAddress = createAddress() - await simulateValidation(badOp, entryPoint.address, { gasLimit: 3e5 }) + const badOpPacked = packUserOp(badOp) + await simulateValidation(badOpPacked, entryPoint.address, { gasLimit: 3e5 }) - const tx = await entryPoint.handleOps([badOp], beneficiaryAddress) // { gasLimit: 3e5 }) + const tx = await entryPoint.handleOps([badOpPacked], beneficiaryAddress) // { gasLimit: 3e5 }) const receipt = await tx.wait() const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason') expect(userOperationRevertReasonEvent?.event).to.equal('UserOperationRevertReason') @@ -325,13 +331,14 @@ describe('EntryPoint', function () { nonce: TOUCH_GET_AGGREGATOR, sender: testWarmColdAccount.address } + const badOpPacked = packUserOp(badOp) const beneficiaryAddress = createAddress() try { - await simulateValidation(badOp, entryPoint.address, { gasLimit: 1e6 }) + await simulateValidation(badOpPacked, entryPoint.address, { gasLimit: 1e6 }) throw new Error('should revert') } catch (e: any) { if ((e as Error).message.includes('ValidationResult')) { - const tx = await entryPoint.handleOps([badOp], beneficiaryAddress, { gasLimit: 1e6 }) + const tx = await entryPoint.handleOps([badOpPacked], beneficiaryAddress, { gasLimit: 1e6 }) await tx.wait() } else { expect(decodeRevertReason(e)).to.include('AA23 reverted') @@ -347,16 +354,18 @@ describe('EntryPoint', function () { const badOp: UserOperation = { ...DefaultsForUserOp, nonce: TOUCH_PAYMASTER, - paymasterAndData: paymaster.address, + paymaster: paymaster.address, + paymasterVerificationGasLimit: 150000, sender: testWarmColdAccount.address } const beneficiaryAddress = createAddress() + const badOpPacked = packUserOp(badOp) try { - await simulateValidation(badOp, entryPoint.address, { gasLimit: 1e6 }) + await simulateValidation(badOpPacked, entryPoint.address, { gasLimit: 1e6 }) throw new Error('should revert') } catch (e: any) { if ((e as Error).message.includes('ValidationResult')) { - const tx = await entryPoint.handleOps([badOp], beneficiaryAddress, { gasLimit: 1e6 }) + const tx = await entryPoint.handleOps([badOpPacked], beneficiaryAddress, { gasLimit: 1e6 }) await tx.wait() } else { expect(decodeRevertReason(e)).to.include('AA23 reverted') @@ -379,7 +388,7 @@ describe('EntryPoint', function () { }) it('should fail nonce with new key and seq!=0', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender, nonce: keyShifted.add(1) }, accountOwner, entryPoint) @@ -388,7 +397,7 @@ describe('EntryPoint', function () { describe('with key=1, seq=1', () => { before(async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender, nonce: keyShifted }, accountOwner, entryPoint) @@ -400,7 +409,7 @@ describe('EntryPoint', function () { }) it('should allow to increment nonce of different key', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender, nonce: await entryPoint.getNonce(sender, key) }, accountOwner, entryPoint) @@ -412,7 +421,7 @@ describe('EntryPoint', function () { const incNonceKey = 5 const incrementCallData = entryPoint.interface.encodeFunctionData('incrementNonce', [incNonceKey]) const callData = account.interface.encodeFunctionData('execute', [entryPoint.address, 0, incrementCallData]) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender, callData, nonce: await entryPoint.getNonce(sender, key) @@ -422,7 +431,7 @@ describe('EntryPoint', function () { expect(await entryPoint.getNonce(sender, incNonceKey)).to.equal(BigNumber.from(incNonceKey).shl(64).add(1)) }) it('should fail with nonsequential seq', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender, nonce: keyShifted.add(3) }, accountOwner, entryPoint) @@ -444,7 +453,7 @@ describe('EntryPoint', function () { it('should revert on signature failure', async () => { // wallet-reported signature failure should revert in handleOps const wrongOwner = createAccountOwner() - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address }, wrongOwner, entryPoint) const beneficiaryAddress = createAddress() @@ -452,7 +461,7 @@ describe('EntryPoint', function () { }) it('account should pay for tx', async function () { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExecFromEntryPoint.data, verificationGasLimit: 1e6, @@ -484,7 +493,7 @@ describe('EntryPoint', function () { const iterations = 45 const count = await counter.populateTransaction.gasWaster(iterations, '') const accountExec = await account.populateTransaction.execute(counter.address, 0, count.data!) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExec.data, verificationGasLimit: 1e5, @@ -517,7 +526,7 @@ describe('EntryPoint', function () { const iterations = 45 const count = await counter.populateTransaction.gasWaster(iterations, '') const accountExec = await account.populateTransaction.execute(counter.address, 0, count.data!) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExec.data, verificationGasLimit: 1e5, @@ -558,10 +567,10 @@ describe('EntryPoint', function () { const beneficiaryAddress = createAddress() // "warmup" userop, for better gas calculation, below - await entryPoint.handleOps([await fillAndSign({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) - await entryPoint.handleOps([await fillAndSign({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) + await entryPoint.handleOps([await fillSignAndPack({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) + await entryPoint.handleOps([await fillSignAndPack({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ sender: account.address, callData: accountExec.data, verificationGasLimit: 1e5, @@ -577,7 +586,7 @@ describe('EntryPoint', function () { const gasUsed1 = logs1[0].args.actualGasUsed.toNumber() const veryBigCallGasLimit = 10000000 - const op2 = await fillAndSign({ + const op2 = await fillSignAndPack({ sender: account.address, callData: accountExec.data, verificationGasLimit: 1e5, @@ -601,7 +610,7 @@ describe('EntryPoint', function () { }) it('legacy mode (maxPriorityFee==maxFeePerGas) should not use "basefee" opcode', async function () { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExecFromEntryPoint.data, maxPriorityFeePerGas: 10e9, @@ -624,7 +633,7 @@ describe('EntryPoint', function () { it('if account has a deposit, it should use it to pay', async function () { await account.addDeposit({ value: ONE_ETH }) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExecFromEntryPoint.data, verificationGasLimit: 1e6, @@ -659,7 +668,7 @@ describe('EntryPoint', function () { }) it('should pay for reverted tx', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: '0xdeadface', verificationGasLimit: 1e6, @@ -680,7 +689,7 @@ describe('EntryPoint', function () { it('#handleOp (single)', async () => { const beneficiaryAddress = createAddress() - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: accountExecFromEntryPoint.data }, accountOwner, entryPoint) @@ -701,7 +710,7 @@ describe('EntryPoint', function () { const callHandleOps = entryPoint.interface.encodeFunctionData('handleOps', [[], beneficiaryAddress]) const execHandlePost = account.interface.encodeFunctionData('execute', [entryPoint.address, 0, callHandleOps]) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, callData: execHandlePost }, accountOwner, entryPoint) @@ -716,14 +725,14 @@ describe('EntryPoint', function () { expect(decodeRevertReason(error?.args?.revertReason)).to.eql('ReentrancyGuardReentrantCall()', 'execution of handleOps inside a UserOp should revert') }) it('should report failure on insufficient verificationGas after creation', async () => { - const op0 = await fillAndSign({ + const op0 = await fillSignAndPack({ sender: account.address, verificationGasLimit: 5e5 }, accountOwner, entryPoint) // must succeed with enough verification gas await simulateValidation(op0, entryPoint.address) - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ sender: account.address, verificationGasLimit: 10000 }, accountOwner, entryPoint) @@ -736,11 +745,11 @@ describe('EntryPoint', function () { if (process.env.COVERAGE != null) { return } - let createOp: UserOperation + let createOp: PackedUserOperation const beneficiaryAddress = createAddress() // 1 it('should reject create if sender address is wrong', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory), verificationGasLimit: 2e6, sender: '0x'.padEnd(42, '1') @@ -752,7 +761,7 @@ describe('EntryPoint', function () { }) it('should reject create if account not funded', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory, 100), verificationGasLimit: 2e6 }, accountOwner, entryPoint) @@ -771,7 +780,7 @@ describe('EntryPoint', function () { const salt = 20 const preAddr = await getAccountAddress(accountOwner.address, simpleAccountFactory, salt) await fund(preAddr) - createOp = await fillAndSign({ + createOp = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory, salt), callGasLimit: 1e6, verificationGasLimit: 2e6 @@ -831,14 +840,14 @@ describe('EntryPoint', function () { await fund(account1) await fund(account2.address) // execute and increment counter - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory), callData: accountExecCounterFromEntryPoint.data, callGasLimit: 2e6, verificationGasLimit: 2e6 }, accountOwner1, entryPoint) - const op2 = await fillAndSign({ + const op2 = await fillSignAndPack({ callData: accountExecCounterFromEntryPoint.data, sender: account2.address, callGasLimit: 2e6, @@ -878,7 +887,7 @@ describe('EntryPoint', function () { await ethersSigner.sendTransaction({ to: aggAccount2.address, value: parseEther('0.1') }) }) it('should fail to execute aggregated account without an aggregator', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: aggAccount.address }, accountOwner, entryPoint) @@ -886,7 +895,7 @@ describe('EntryPoint', function () { await expect(entryPoint.handleOps([userOp], beneficiaryAddress)).to.revertedWith('AA24 signature error') }) it('should fail to execute aggregated account with wrong aggregator', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: aggAccount.address }, accountOwner, entryPoint) @@ -905,7 +914,7 @@ describe('EntryPoint', function () { const address1 = hexZeroPad('0x1', 20) const aggAccount1 = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, address1) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: aggAccount1.address, maxFeePerGas: 0 }, accountOwner, entryPoint) @@ -922,7 +931,7 @@ describe('EntryPoint', function () { }) it('should fail to execute aggregated account with wrong agg. signature', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: aggAccount.address }, accountOwner, entryPoint) @@ -941,16 +950,16 @@ describe('EntryPoint', function () { const aggAccount3 = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, aggregator3.address) await ethersSigner.sendTransaction({ to: aggAccount3.address, value: parseEther('0.1') }) - const userOp1 = await fillAndSign({ + const userOp1 = await fillSignAndPack({ sender: aggAccount.address }, accountOwner, entryPoint) - const userOp2 = await fillAndSign({ + const userOp2 = await fillSignAndPack({ sender: aggAccount2.address }, accountOwner, entryPoint) - const userOp_agg3 = await fillAndSign({ + const userOp_agg3 = await fillSignAndPack({ sender: aggAccount3.address }, accountOwner, entryPoint) - const userOp_noAgg = await fillAndSign({ + const userOp_noAgg = await fillSignAndPack({ sender: account.address }, accountOwner, entryPoint) @@ -1016,13 +1025,13 @@ describe('EntryPoint', function () { context('create account', () => { let initCode: BytesLike let addr: string - let userOp: UserOperation + let userOp: PackedUserOperation before(async () => { const factory = await new TestAggregatedAccountFactory__factory(ethersSigner).deploy(entryPoint.address, aggregator.address) initCode = await getAggregatedAccountInitCode(entryPoint.address, factory) addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') }) - userOp = await fillAndSign({ + userOp = await fillSignAndPack({ initCode }, accountOwner, entryPoint) }) @@ -1062,8 +1071,9 @@ describe('EntryPoint', function () { it('should fail with nonexistent paymaster', async () => { const pm = createAddress() - const op = await fillAndSign({ - paymasterAndData: pm, + const op = await fillSignAndPack({ + paymaster: pm, + paymasterVerificationGasLimit: 3e6, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(account2Owner.address, simpleAccountFactory), verificationGasLimit: 3e6, @@ -1073,8 +1083,9 @@ describe('EntryPoint', function () { }) it('should fail if paymaster has no deposit', async function () { - const op = await fillAndSign({ - paymasterAndData: paymaster.address, + const op = await fillSignAndPack({ + paymaster: paymaster.address, + paymasterVerificationGasLimit: 3e6, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(account2Owner.address, simpleAccountFactory), @@ -1092,8 +1103,10 @@ describe('EntryPoint', function () { await errorPostOp.addStake(globalUnstakeDelaySec, { value: paymasterStake }) await errorPostOp.deposit({ value: ONE_ETH }) - const op = await fillAndSign({ - paymasterAndData: errorPostOp.address, + const op = await fillSignAndPack({ + paymaster: errorPostOp.address, + paymasterPostOpGasLimit: 1e5, + paymasterVerificationGasLimit: 3e6, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(account3Owner.address, simpleAccountFactory), @@ -1116,8 +1129,8 @@ describe('EntryPoint', function () { await errorPostOp.addStake(globalUnstakeDelaySec, { value: paymasterStake }) await errorPostOp.deposit({ value: ONE_ETH }) - const op = await fillAndSign({ - paymasterAndData: errorPostOp.address, + const op = await fillSignAndPack({ + paymaster: errorPostOp.address, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(account3Owner.address, simpleAccountFactory), @@ -1132,8 +1145,9 @@ describe('EntryPoint', function () { it('paymaster should pay for tx', async function () { await paymaster.deposit({ value: ONE_ETH }) - const op = await fillAndSign({ - paymasterAndData: paymaster.address, + const op = await fillSignAndPack({ + paymaster: paymaster.address, + paymasterVerificationGasLimit: 1e6, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(account2Owner.address, simpleAccountFactory) }, account2Owner, entryPoint) @@ -1149,8 +1163,9 @@ describe('EntryPoint', function () { await paymaster.deposit({ value: ONE_ETH }) const anOwner = createAccountOwner() - const op = await fillAndSign({ - paymasterAndData: paymaster.address, + const op = await fillSignAndPack({ + paymaster: paymaster.address, + paymasterVerificationGasLimit: 1e6, callData: accountExecFromEntryPoint.data, initCode: getAccountInitCode(anOwner.address, simpleAccountFactory) }, anOwner, entryPoint) @@ -1183,7 +1198,7 @@ describe('EntryPoint', function () { describe('validateUserOp time-range', function () { it('should accept non-expired owner', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address }, sessionOwner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) @@ -1194,7 +1209,7 @@ describe('EntryPoint', function () { it('should not reject expired owner', async () => { const expiredOwner = createAccountOwner() await account.addTemporaryOwner(expiredOwner.address, 123, now - 60) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address }, expiredOwner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) @@ -1216,9 +1231,10 @@ describe('EntryPoint', function () { it('should accept non-expired paymaster request', async () => { const timeRange = defaultAbiCoder.encode(['uint48', 'uint48'], [123, now + 60]) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, timeRange]) + paymaster: paymaster.address, + paymasterData: timeRange }, ethersSigner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) expect(ret.returnInfo.validUntil).to.eql(now + 60) @@ -1227,9 +1243,10 @@ describe('EntryPoint', function () { it('should not reject expired paymaster request', async () => { const timeRange = defaultAbiCoder.encode(['uint48', 'uint48'], [321, now - 60]) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, timeRange]) + paymaster: paymaster.address, + paymasterData: timeRange }, ethersSigner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) expect(ret.returnInfo.validUntil).to.eql(now - 60) @@ -1237,11 +1254,12 @@ describe('EntryPoint', function () { }) // helper method - async function createOpWithPaymasterParams (owner: Wallet, after: number, until: number): Promise { + async function createOpWithPaymasterParams (owner: Wallet, after: number, until: number): Promise { const timeRange = defaultAbiCoder.encode(['uint48', 'uint48'], [after, until]) - return await fillAndSign({ + return await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, timeRange]) + paymaster: paymaster.address, + paymasterData: timeRange }, owner, entryPoint) } @@ -1283,7 +1301,7 @@ describe('EntryPoint', function () { it('should revert on expired account', async () => { const expiredOwner = createAccountOwner() await account.addTemporaryOwner(expiredOwner.address, 1, 2) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address }, expiredOwner, entryPoint) await expect(entryPoint.handleOps([userOp], beneficiary)) @@ -1293,7 +1311,7 @@ describe('EntryPoint', function () { it('should revert on date owner', async () => { const futureOwner = createAccountOwner() await account.addTemporaryOwner(futureOwner.address, now + 100, now + 200) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address }, futureOwner, entryPoint) await expect(entryPoint.handleOps([userOp], beneficiary)) diff --git a/test/entrypointsimulations.test.ts b/test/entrypointsimulations.test.ts index 4a3a5108..33126067 100644 --- a/test/entrypointsimulations.test.ts +++ b/test/entrypointsimulations.test.ts @@ -18,7 +18,7 @@ import { getBalance, deployEntryPoint } from './testutils' -import { fillAndSign, simulateHandleOp, simulateValidation } from './UserOp' +import { fillSignAndPack, simulateHandleOp, simulateValidation } from './UserOp' import { BigNumber, Wallet } from 'ethers' import { hexConcat } from 'ethers/lib/utils' @@ -116,7 +116,7 @@ describe('EntryPointSimulations', function () { it('should fail if validateUserOp fails', async () => { // using wrong nonce - const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint) + const op = await fillSignAndPack({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint) await expect(simulateValidation(op, entryPoint.address)).to .revertedWith('AA25 invalid account nonce') }) @@ -125,13 +125,13 @@ describe('EntryPointSimulations', function () { // (this is actually a feature of the wallet, not the entrypoint) // using wrong owner for account1 // (zero gas price so that it doesn't fail on prefund) - const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner, entryPoint) + const op = await fillSignAndPack({ sender: account1.address, maxFeePerGas: 0 }, accountOwner, entryPoint) const { returnInfo } = await simulateValidation(op, entryPoint.address) expect(returnInfo.sigFailed).to.be.true }) it('should revert if wallet not deployed (and no initCode)', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: createAddress(), nonce: 0, verificationGasLimit: 1000 @@ -141,19 +141,19 @@ describe('EntryPointSimulations', function () { }) it('should revert on oog if not enough verificationGas', async () => { - const op = await fillAndSign({ sender: account.address, verificationGasLimit: 1000 }, accountOwner, entryPoint) + const op = await fillSignAndPack({ sender: account.address, verificationGasLimit: 1000 }, accountOwner, entryPoint) await expect(simulateValidation(op, entryPoint.address)).to .revertedWith('AA23 reverted') }) it('should succeed if validateUserOp succeeds', async () => { - const op = await fillAndSign({ sender: account1.address }, accountOwner1, entryPoint) + const op = await fillSignAndPack({ sender: account1.address }, accountOwner1, entryPoint) await fund(account1) await simulateValidation(op, entryPoint.address) }) it('should return empty context if no paymaster', async () => { - const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner1, entryPoint) + const op = await fillSignAndPack({ sender: account1.address, maxFeePerGas: 0 }, accountOwner1, entryPoint) const { returnInfo } = await simulateValidation(op, entryPoint.address) expect(returnInfo.paymasterContext).to.eql('0x') }) @@ -164,14 +164,14 @@ describe('EntryPointSimulations', function () { const { proxy: account2 } = await createAccount(ethersSigner, await ethersSigner.getAddress(), entryPoint.address) await fund(account2) await account2.execute(entryPoint.address, stakeValue, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay])) - const op = await fillAndSign({ sender: account2.address }, ethersSigner, entryPoint) + const op = await fillSignAndPack({ sender: account2.address }, ethersSigner, entryPoint) const result = await simulateValidation(op, entryPoint.address) expect(result.senderInfo.stake).to.equal(stakeValue) expect(result.senderInfo.unstakeDelaySec).to.equal(unstakeDelay) }) it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ preVerificationGas: BigNumber.from(2).pow(130), sender: account1.address }, accountOwner1, entryPoint) @@ -181,7 +181,7 @@ describe('EntryPointSimulations', function () { }) it('should fail creation for wrong sender', async () => { - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory), sender: '0x'.padEnd(42, '1'), verificationGasLimit: 30e6 @@ -193,7 +193,7 @@ describe('EntryPointSimulations', function () { it('should report failure on insufficient verificationGas (OOG) for creation', async () => { const initCode = getAccountInitCode(accountOwner1.address, simpleAccountFactory) const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - const op0 = await fillAndSign({ + const op0 = await fillSignAndPack({ initCode, sender, verificationGasLimit: 5e5, @@ -202,7 +202,7 @@ describe('EntryPointSimulations', function () { // must succeed with enough verification gas. await simulateValidation(op0, entryPoint.address, { gas: '0xF4240' }) - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ initCode, sender, verificationGasLimit: 1e5, @@ -214,7 +214,7 @@ describe('EntryPointSimulations', function () { it('should succeed for creating an account', async () => { const sender = await getAccountAddress(accountOwner1.address, simpleAccountFactory) - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ sender, initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory) }, accountOwner1, entryPoint) @@ -227,7 +227,7 @@ describe('EntryPointSimulations', function () { // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. const { proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address) const sender = createAddress() - const op1 = await fillAndSign({ + const op1 = await fillSignAndPack({ initCode: hexConcat([ account.address, account.interface.encodeFunctionData('execute', [sender, 0, '0x']) @@ -249,7 +249,7 @@ describe('EntryPointSimulations', function () { const count = counter.interface.encodeFunctionData('count') const callData = account.interface.encodeFunctionData('execute', [counter.address, 0, count]) // deliberately broken signature. simulate should work with it too. - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, callData }, accountOwner1, entryPoint) diff --git a/test/paymaster.test.ts b/test/paymaster.test.ts index 5e5e0544..a82664c8 100644 --- a/test/paymaster.test.ts +++ b/test/paymaster.test.ts @@ -24,9 +24,9 @@ import { createAccount, getAccountAddress, decodeRevertReason } from './testutils' -import { fillAndSign, simulateValidation } from './UserOp' +import { fillSignAndPack, simulateValidation } from './UserOp' import { hexConcat, parseEther } from 'ethers/lib/utils' -import { UserOperation } from './UserOperation' +import { PackedUserOperation } from './UserOperation' import { hexValue } from '@ethersproject/bytes' describe('EntryPoint with paymaster', function () { @@ -104,9 +104,10 @@ describe('EntryPoint with paymaster', function () { calldata = await account.populateTransaction.execute(account.address, 0, updateEntryPoint).then(tx => tx.data!) }) it('paymaster should reject if account doesn\'t have tokens', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: account.address, - paymasterAndData: paymaster.address, + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5, callData: calldata }, accountOwner, entryPoint) expect(await entryPoint.callStatic.handleOps([op], beneficiaryAddress, { @@ -121,15 +122,16 @@ describe('EntryPoint with paymaster', function () { }) describe('create account', () => { - let createOp: UserOperation + let createOp: PackedUserOperation let created = false const beneficiaryAddress = createAddress() it('should reject if account not funded', async () => { - const op = await fillAndSign({ + const op = await fillSignAndPack({ initCode: getAccountDeployer(entryPoint.address, accountOwner.address, 1), verificationGasLimit: 1e7, - paymasterAndData: paymaster.address + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5 }, accountOwner, entryPoint) expect(await entryPoint.callStatic.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 @@ -138,10 +140,11 @@ describe('EntryPoint with paymaster', function () { }) it('should succeed to create account with tokens', async () => { - createOp = await fillAndSign({ + createOp = await fillSignAndPack({ initCode: getAccountDeployer(entryPoint.address, accountOwner.address, 3), verificationGasLimit: 2e6, - paymasterAndData: paymaster.address, + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5, nonce: 0 }, accountOwner, entryPoint) @@ -192,16 +195,17 @@ describe('EntryPoint with paymaster', function () { const justEmit = testCounter.interface.encodeFunctionData('justemit') const execFromSingleton = account.interface.encodeFunctionData('execute', [testCounter.address, 0, justEmit]) - const ops: UserOperation[] = [] + const ops: PackedUserOperation[] = [] const accounts: SimpleAccount[] = [] for (let i = 0; i < 4; i++) { const { proxy: aAccount } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address) await paymaster.mintTokens(aAccount.address, parseEther('1')) - const op = await fillAndSign({ + const op = await fillSignAndPack({ sender: aAccount.address, callData: execFromSingleton, - paymasterAndData: paymaster.address + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5 }, accountOwner, entryPoint) accounts.push(aAccount) @@ -234,10 +238,11 @@ describe('EntryPoint with paymaster', function () { await paymaster.mintTokens(account.address, parseEther('1')) approveCallData = paymaster.interface.encodeFunctionData('approve', [account.address, ethers.constants.MaxUint256]) // need to call approve from account2. use paymaster for that - const approveOp = await fillAndSign({ + const approveOp = await fillSignAndPack({ sender: account2.address, callData: account2.interface.encodeFunctionData('execute', [paymaster.address, 0, approveCallData]), - paymasterAndData: paymaster.address + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5 }, accountOwner, entryPoint) await entryPoint.handleOps([approveOp], beneficiaryAddress) expect(await paymaster.allowance(account2.address, account.address)).to.eq(ethers.constants.MaxUint256) @@ -252,17 +257,19 @@ describe('EntryPoint with paymaster', function () { const withdrawTokens = paymaster.interface.encodeFunctionData('transferFrom', [account2.address, account.address, withdrawAmount]) const execFromEntryPoint = account.interface.encodeFunctionData('execute', [paymaster.address, 0, withdrawTokens]) - const userOp1 = await fillAndSign({ + const userOp1 = await fillSignAndPack({ sender: account.address, callData: execFromEntryPoint, - paymasterAndData: paymaster.address + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5 }, accountOwner, entryPoint) // account2's operation is unimportant, as it is going to be reverted - but the paymaster will have to pay for it. - const userOp2 = await fillAndSign({ + const userOp2 = await fillSignAndPack({ sender: account2.address, callData: execFromEntryPoint, - paymasterAndData: paymaster.address, + paymaster: paymaster.address, + paymasterPostOpGasLimit: 3e5, callGasLimit: 1e6 }, accountOwner, entryPoint) diff --git a/test/samples/TokenPaymaster.test.ts b/test/samples/TokenPaymaster.test.ts index 35d7aada..f59e785a 100644 --- a/test/samples/TokenPaymaster.test.ts +++ b/test/samples/TokenPaymaster.test.ts @@ -1,5 +1,5 @@ -import { BigNumberish, ContractReceipt, ContractTransaction, Wallet, utils, BigNumber } from 'ethers' -import { Interface, parseEther } from 'ethers/lib/utils' +import { ContractReceipt, ContractTransaction, Wallet, utils, BigNumber } from 'ethers' +import { hexlify, hexZeroPad, Interface, parseEther } from 'ethers/lib/utils' import { assert, expect } from 'chai' import { ethers } from 'hardhat' @@ -33,19 +33,7 @@ import { fund, objdump } from '../testutils' -import { fillUserOp, signUserOp } from '../UserOp' - -function generatePaymasterAndData (pm: string, tokenPrice?: BigNumberish): string { - if (tokenPrice != null) { - return utils.hexlify( - utils.concat([pm, utils.hexZeroPad(utils.hexlify(tokenPrice), 32)]) - ) - } else { - return utils.hexlify( - utils.concat([pm]) - ) - } -} +import { fillUserOp, packUserOp, signUserOp } from '../UserOp' const priceDenominator = BigNumber.from(10).pow(26) @@ -146,21 +134,21 @@ describe('TokenPaymaster', function () { it('paymaster should reject if account does not have enough tokens or allowance', async () => { const snapshot = await ethers.provider.send('evm_snapshot', []) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) // await expect( - expect(await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + expect(await entryPoint.handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) .catch(e => decodeRevertReason(e))) .to.match(/FailedOpWithRevert\(0,"AA33 reverted",ERC20InsufficientAllowance/) await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - expect(await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + expect(await entryPoint.handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) .catch(e => decodeRevertReason(e))) .to.match(/FailedOpWithRevert\(0,"AA33 reverted",ERC20InsufficientBalance/) @@ -172,17 +160,19 @@ describe('TokenPaymaster', function () { await token.transfer(account.address, parseEther('1')) await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) // for simpler 'gasPrice()' calculation await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 3e7, maxFeePerGas: op.maxFeePerGas, maxPriorityFeePerGas: op.maxFeePerGas @@ -214,7 +204,7 @@ describe('TokenPaymaster', function () { assert.equal(actualTokenChargeEvents.toString(), actualTokenCharge.toString()) assert.equal(actualTokenChargeEvents.toString(), expectedTokenCharge.toString()) assert.equal(actualTokenPrice / (priceDenominator as any), expectedTokenPrice) - assert.closeTo(postOpGasCost.div(tx.effectiveGasPrice).toNumber(), 40000, 20000) + assert.closeTo(postOpGasCost.div(tx.effectiveGasPrice).toNumber(), 50000, 20000) await ethers.provider.send('evm_revert', [snapshot]) }) @@ -225,15 +215,17 @@ describe('TokenPaymaster', function () { await tokenOracle.setPrice(initialPriceToken * 5) await nativeAssetOracle.setPrice(initialPriceEther * 10) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) const tx: ContractTransaction = await entryPoint - .handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) const receipt: ContractReceipt = await tx.wait() const block = await ethers.provider.getBlock(receipt.blockHash) @@ -262,19 +254,22 @@ describe('TokenPaymaster', function () { const currentCachedPrice = await paymaster.cachedPrice() assert.equal((currentCachedPrice as any) / (priceDenominator as any), 0.2) const overrideTokenPrice = priceDenominator.mul(132).div(1000) - const paymasterAndData = generatePaymasterAndData(paymasterAddress, overrideTokenPrice) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, + paymasterData: hexZeroPad(hexlify(overrideTokenPrice), 32), callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) // for simpler 'gasPrice()' calculation await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7, maxFeePerGas: op.maxFeePerGas, maxPriorityFeePerGas: op.maxFeePerGas @@ -286,7 +281,7 @@ describe('TokenPaymaster', function () { }) const preChargeTokens = decodedLogs[0].args.value - const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).mul(2)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) + const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).add(BigNumber.from(op.paymasterVerificationGasLimit))).add(BigNumber.from(op.paymasterPostOpGasLimit)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) const requiredPrefund = requiredGas.mul(op.maxFeePerGas) const preChargeTokenPrice = requiredPrefund.mul(priceDenominator).div(preChargeTokens) @@ -304,19 +299,22 @@ describe('TokenPaymaster', function () { assert.equal((currentCachedPrice as any) / (priceDenominator as any), 0.2) // note: higher number is lower token price const overrideTokenPrice = priceDenominator.mul(50) - const paymasterAndData = generatePaymasterAndData(paymasterAddress, overrideTokenPrice) let op = await fillUserOp({ sender: account.address, maxFeePerGas: 1000000000, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, + paymasterData: hexZeroPad(hexlify(overrideTokenPrice), 32), callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) // for simpler 'gasPrice()' calculation await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7, maxFeePerGas: op.maxFeePerGas, maxPriorityFeePerGas: op.maxFeePerGas @@ -328,7 +326,7 @@ describe('TokenPaymaster', function () { }) const preChargeTokens = decodedLogs[0].args.value - const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).mul(2)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) + const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).add(BigNumber.from(op.paymasterVerificationGasLimit))).add(BigNumber.from(op.paymasterPostOpGasLimit)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) const requiredPrefund = requiredGas.mul(op.maxFeePerGas) const preChargeTokenPrice = requiredPrefund.mul(priceDenominator).div(preChargeTokens) @@ -347,15 +345,17 @@ describe('TokenPaymaster', function () { // Cannot happen too fast though await ethers.provider.send('evm_increaseTime', [200]) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) .then(async tx => await tx.wait()) const decodedLogs = tx.logs.map(it => { @@ -393,15 +393,17 @@ describe('TokenPaymaster', function () { const withdrawTokensCall = await token.populateTransaction.transfer(token.address, parseEther('0.009')).then(tx => tx.data!) const callData = await account.populateTransaction.execute(token.address, 0, withdrawTokensCall).then(tx => tx.data!) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) .then(async tx => await tx.wait()) const decodedLogs = tx.logs.map(it => { @@ -427,15 +429,17 @@ describe('TokenPaymaster', function () { // deposit exactly the minimum amount so the next UserOp makes it go under await entryPoint.depositTo(paymaster.address, { value: minEntryPointBalance }) - const paymasterAndData = generatePaymasterAndData(paymasterAddress) let op = await fillUserOp({ sender: account.address, - paymasterAndData, + paymaster: paymasterAddress, + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 3e5, callData }, entryPoint) op = signUserOp(op, accountOwner, entryPoint.address, chainId) + const opPacked = packUserOp(op) const tx = await entryPoint - .handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) .then(async tx => await tx.wait()) const decodedLogs = tx.logs.map(it => { return testInterface.parseLog(it) diff --git a/test/simple-wallet.test.ts b/test/simple-wallet.test.ts index 12d2ca70..844bd37e 100644 --- a/test/simple-wallet.test.ts +++ b/test/simple-wallet.test.ts @@ -20,7 +20,7 @@ import { ONE_ETH, HashZero, deployEntryPoint } from './testutils' -import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp' +import { fillUserOpDefaults, getUserOpHash, encodeUserOp, signUserOp, packUserOp } from './UserOp' import { parseEther } from 'ethers/lib/utils' import { UserOperation } from './UserOperation' @@ -53,8 +53,9 @@ describe('SimpleAccount', function () { it('should pack in js the same as solidity', async () => { const op = await fillUserOpDefaults({ sender: accounts[0] }) + const encoded = encodeUserOp(op) const packed = packUserOp(op) - expect(await testUtil.packUserOp(op)).to.equal(packed) + expect(await testUtil.encodeUserOp(packed)).to.equal(encoded) }) describe('#executeBatch', () => { @@ -135,7 +136,8 @@ describe('SimpleAccount', function () { expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit) preBalance = await getBalance(account.address) - const ret = await account.validateUserOp(userOp, userOpHash, expectedPay, { gasPrice: actualGasPrice }) + const packedOp = packUserOp(userOp) + const ret = await account.validateUserOp(packedOp, userOpHash, expectedPay, { gasPrice: actualGasPrice }) await ret.wait() }) @@ -146,7 +148,8 @@ describe('SimpleAccount', function () { it('should return NO_SIG_VALIDATION on wrong signature', async () => { const userOpHash = HashZero - const deadline = await account.callStatic.validateUserOp({ ...userOp, nonce: 1 }, userOpHash, 0) + const packedOp = packUserOp(userOp) + const deadline = await account.callStatic.validateUserOp({ ...packedOp, nonce: 1 }, userOpHash, 0) expect(deadline).to.eq(1) }) }) diff --git a/test/solidityTypes.ts b/test/solidityTypes.ts index 5026ef9e..5a051e99 100644 --- a/test/solidityTypes.ts +++ b/test/solidityTypes.ts @@ -6,5 +6,6 @@ export type address = string export type uint256 = BigNumberish export type uint = BigNumberish export type uint48 = BigNumberish +export type uint128 = BigNumberish export type bytes = BytesLike export type bytes32 = BytesLike diff --git a/test/testExecAccount.test.ts b/test/testExecAccount.test.ts index 4a1e5035..312a270f 100644 --- a/test/testExecAccount.test.ts +++ b/test/testExecAccount.test.ts @@ -6,7 +6,7 @@ import { TestExecAccountFactory__factory } from '../typechain' import { createAccountOwner, deployEntryPoint, fund, objdump } from './testutils' -import { fillAndSign } from './UserOp' +import { fillSignAndPack } from './UserOp' import { Signer, Wallet } from 'ethers' import { ethers } from 'hardhat' import { defaultAbiCoder, hexConcat, hexStripZeros } from 'ethers/lib/utils' @@ -37,7 +37,7 @@ describe('IAccountExecute', () => { account.interface.encodeFunctionData('entryPoint') ]) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, callGasLimit: 100000, // normal estimate also chokes on this callData callData: hexConcat([execSig, innerCall]) diff --git a/test/testutils.ts b/test/testutils.ts index 144b044b..a19b58b8 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -1,7 +1,10 @@ import { ethers } from 'hardhat' import { arrayify, - hexConcat, Interface, + hexConcat, + hexlify, + hexZeroPad, + Interface, keccak256, parseEther } from 'ethers/lib/utils' @@ -10,18 +13,16 @@ import { EntryPoint, EntryPoint__factory, IERC20, - IEntryPoint, SimpleAccount, SimpleAccountFactory__factory, SimpleAccount__factory, SimpleAccountFactory, TestAggregatedAccountFactory, TestPaymasterRevertCustomError__factory, TestERC20__factory } from '../typechain' -import { BytesLike } from '@ethersproject/bytes' +import { BytesLike, Hexable } from '@ethersproject/bytes' import { expect } from 'chai' import { Create2Factory } from '../src/Create2Factory' import { debugTransaction } from './debugTx' -import { UserOperation } from './UserOperation' export const AddressZero = ethers.constants.AddressZero export const HashZero = ethers.constants.HashZero @@ -279,15 +280,6 @@ export async function isDeployed (addr: string): Promise { return code.length > 2 } -// internal helper function: create a UserOpsPerAggregator structure, with no aggregator or signature -export function userOpsWithoutAgg (userOps: UserOperation[]): IEntryPoint.UserOpsPerAggregatorStruct[] { - return [{ - userOps, - aggregator: AddressZero, - signature: '0x' - }] -} - // Deploys an implementation and a proxy pointing to this implementation export async function createAccount ( ethersSigner: Signer, @@ -311,3 +303,20 @@ export async function createAccount ( proxy } } + +export function packAccountGasLimits (validationGasLimit: BigNumberish, callGasLimit: BigNumberish): string { + return ethers.utils.hexConcat([ + hexZeroPad(hexlify(validationGasLimit, { hexPad: 'left' }), 16), hexZeroPad(hexlify(callGasLimit, { hexPad: 'left' }), 16) + ]) +} + +export function packPaymasterData (paymaster: string, paymasterVerificationGasLimit: BytesLike | Hexable | number | bigint, postOpGasLimit: BytesLike | Hexable | number | bigint, paymasterData: string): string { + return ethers.utils.hexConcat([ + paymaster, hexZeroPad(hexlify(paymasterVerificationGasLimit, { hexPad: 'left' }), 16), + hexZeroPad(hexlify(postOpGasLimit, { hexPad: 'left' }), 16), paymasterData + ]) +} + +export function unpackAccountGasLimits (accountGasLimits: string): { validationGasLimit: number, callGasLimit: number } { + return { validationGasLimit: parseInt(accountGasLimits.slice(2, 34), 16), callGasLimit: parseInt(accountGasLimits.slice(34), 16) } +} diff --git a/test/verifying_paymaster.test.ts b/test/verifying_paymaster.test.ts index b95d92f8..4db471d1 100644 --- a/test/verifying_paymaster.test.ts +++ b/test/verifying_paymaster.test.ts @@ -10,11 +10,11 @@ import { import { createAccount, createAccountOwner, createAddress, decodeRevertReason, - deployEntryPoint + deployEntryPoint, packPaymasterData } from './testutils' -import { fillAndSign, simulateValidation } from './UserOp' +import { DefaultsForUserOp, fillAndSign, fillSignAndPack, packUserOp, simulateValidation } from './UserOp' import { arrayify, defaultAbiCoder, hexConcat, parseEther } from 'ethers/lib/utils' -import { UserOperation } from './UserOperation' +import { PackedUserOperation } from './UserOperation' const MOCK_VALID_UNTIL = '0x00000000deadbeef' const MOCK_VALID_AFTER = '0x0000000000001234' @@ -43,9 +43,20 @@ describe('EntryPoint with VerifyingPaymaster', function () { describe('#parsePaymasterAndData', () => { it('should parse data properly', async () => { - const paymasterAndData = hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), MOCK_SIG]) + const paymasterAndData = packPaymasterData( + paymaster.address, + DefaultsForUserOp.paymasterVerificationGasLimit, + DefaultsForUserOp.paymasterPostOpGasLimit, + hexConcat([ + defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), MOCK_SIG + ]) + ) console.log(paymasterAndData) const res = await paymaster.parsePaymasterAndData(paymasterAndData) + // console.log('MOCK_VALID_UNTIL, MOCK_VALID_AFTER', MOCK_VALID_UNTIL, MOCK_VALID_AFTER) + // console.log('validUntil after', res.validUntil, res.validAfter) + // console.log('MOCK SIG', MOCK_SIG) + // console.log('sig', res.signature) expect(res.validUntil).to.be.equal(ethers.BigNumber.from(MOCK_VALID_UNTIL)) expect(res.validAfter).to.be.equal(ethers.BigNumber.from(MOCK_VALID_AFTER)) expect(res.signature).equal(MOCK_SIG) @@ -54,9 +65,10 @@ describe('EntryPoint with VerifyingPaymaster', function () { describe('#validatePaymasterUserOp', () => { it('should reject on no signature', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x1234']) + paymaster: paymaster.address, + paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x1234']) }, accountOwner, entryPoint) expect(await simulateValidation(userOp, entryPoint.address) .catch(e => decodeRevertReason(e))) @@ -64,9 +76,11 @@ describe('EntryPoint with VerifyingPaymaster', function () { }) it('should reject on invalid signature', async () => { - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) + paymaster: paymaster.address, + paymasterData: hexConcat( + [defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) }, accountOwner, entryPoint) expect(await simulateValidation(userOp, entryPoint.address) .catch(e => decodeRevertReason(e))) @@ -74,13 +88,14 @@ describe('EntryPoint with VerifyingPaymaster', function () { }) describe('with wrong signature', () => { - let wrongSigUserOp: UserOperation + let wrongSigUserOp: PackedUserOperation const beneficiaryAddress = createAddress() before(async () => { const sig = await offchainSigner.signMessage(arrayify('0xdead')) - wrongSigUserOp = await fillAndSign({ + wrongSigUserOp = await fillSignAndPack({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) + paymaster: paymaster.address, + paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) }, accountOwner, entryPoint) }) @@ -97,13 +112,16 @@ describe('EntryPoint with VerifyingPaymaster', function () { it('succeed with valid signature', async () => { const userOp1 = await fillAndSign({ sender: account.address, - paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) + paymaster: paymaster.address, + paymasterData: hexConcat( + [defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) }, accountOwner, entryPoint) - const hash = await paymaster.getHash(userOp1, MOCK_VALID_UNTIL, MOCK_VALID_AFTER) + const hash = await paymaster.getHash(packUserOp(userOp1), MOCK_VALID_UNTIL, MOCK_VALID_AFTER) const sig = await offchainSigner.signMessage(arrayify(hash)) - const userOp = await fillAndSign({ + const userOp = await fillSignAndPack({ ...userOp1, - paymasterAndData: hexConcat([paymaster.address, defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) + paymaster: paymaster.address, + paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) }, accountOwner, entryPoint) const res = await simulateValidation(userOp, entryPoint.address) expect(res.returnInfo.sigFailed).to.be.false diff --git a/test/y.bls.test.ts b/test/y.bls.test.ts index 66305bc8..2f91b41b 100644 --- a/test/y.bls.test.ts +++ b/test/y.bls.test.ts @@ -13,7 +13,7 @@ import { } from '../typechain' import { ethers } from 'hardhat' import { createAddress, deployEntryPoint, fund, ONE_ETH } from './testutils' -import { DefaultsForUserOp, fillUserOp, simulateValidation } from './UserOp' +import { DefaultsForUserOp, fillAndPack, packUserOp, simulateValidation } from './UserOp' import { expect } from 'chai' import { keccak256 } from 'ethereumjs-util' import { hashToPoint } from '@thehubbleproject/bls/dist/mcl' @@ -66,14 +66,14 @@ describe('bls account', function () { const sig1 = signer1.sign('0x1234') const sig2 = signer2.sign('0x5678') const offChainSigResult = hexConcat(aggregate([sig1, sig2])) - const userOp1 = { ...DefaultsForUserOp, signature: hexConcat(sig1) } - const userOp2 = { ...DefaultsForUserOp, signature: hexConcat(sig2) } + const userOp1 = packUserOp({ ...DefaultsForUserOp, signature: hexConcat(sig1) }) + const userOp2 = packUserOp({ ...DefaultsForUserOp, signature: hexConcat(sig2) }) const solidityAggResult = await blsAgg.aggregateSignatures([userOp1, userOp2]) expect(solidityAggResult).to.equal(offChainSigResult) }) it('#userOpToMessage', async () => { - const userOp1 = await fillUserOp({ + const userOp1 = await fillAndPack({ sender: account1.address }, entrypoint) const requestHash = await blsAgg.getUserOpHash(userOp1) @@ -83,7 +83,7 @@ describe('bls account', function () { }) it('#validateUserOpSignature', async () => { - const userOp1 = await fillUserOp({ + const userOp1 = await fillAndPack({ sender: account1.address }, entrypoint) const requestHash = await blsAgg.getUserOpHash(userOp1) @@ -108,7 +108,7 @@ describe('bls account', function () { const res = await brokenAccountFactory.provider.call(deployTx) const acc = brokenAccountFactory.interface.decodeFunctionResult('createAccount', res)[0] await fund(acc) - const userOp = await fillUserOp({ + const userOp = await fillAndPack({ sender: acc, initCode: hexConcat([brokenAccountFactory.address, deployTx.data!]) }, entrypoint) @@ -135,14 +135,14 @@ describe('bls account', function () { it('validateSignatures', async function () { // yes, it does take long on hardhat, but quick on geth. this.timeout(30000) - const userOp1 = await fillUserOp({ + const userOp1 = await fillAndPack({ sender: account1.address }, entrypoint) const requestHash = await blsAgg.getUserOpHash(userOp1) const sig1 = signer1.sign(requestHash) userOp1.signature = hexConcat(sig1) - const userOp2 = await fillUserOp({ + const userOp2 = await fillAndPack({ sender: account2.address }, entrypoint) const requestHash2 = await blsAgg.getUserOpHash(userOp2) @@ -182,7 +182,7 @@ describe('bls account', function () { const verifier = new BlsVerifier(BLS_DOMAIN) const senderAddress = await entrypoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) await fund(senderAddress, '0.01') - const userOp = await fillUserOp({ + const userOp = await fillAndPack({ sender: senderAddress, initCode }, entrypoint)