diff --git a/src/modules/user-ops/userop.service.ts b/src/modules/user-ops/userop.service.ts index 97d0bfca..62d5a7f1 100644 --- a/src/modules/user-ops/userop.service.ts +++ b/src/modules/user-ops/userop.service.ts @@ -5,6 +5,7 @@ import { ChainsService, } from "@/chains"; import { unixTimestamp, withTrace } from "@/common"; +import { BadRequestException } from "@/common/exceptions/bad-request.exception"; import { validateTimestamps } from "@/common/utils/timestamp"; import { ContractsService } from "@/contracts"; import { type ConfigType, InjectConfig } from "@/core/config"; @@ -359,6 +360,40 @@ export class UserOpService { "Got gas limits", ); + // The batcher will never include a userOp whose execution gas limit exceeds the chain's + // batch gas limit cap - it would be silently dropped ("Invalid maxGasLimit") after the + // payment userOp has already executed and charged the user. Reject at quote time instead + // so the client gets a clear error before any funds move. + // The formula mirrors the batcher check: preVerificationGas is excluded because it is + // not spent during execution. + if (!isTrustedPaymentUserOp) { + const executionGasLimit = + verificationGasLimit + + callGasLimit + + paymasterVerificationGasLimit + + postOpGasLimit; + + const { + batcher: { batchGasLimit }, + } = chainConfig; + + if (executionGasLimit > batchGasLimit) { + this.logger.error( + { + executionGasLimit, + batchGasLimit, + chainId, + sender: userOpRequest.sender, + }, + "UserOp gas limit exceeds the chain batch gas limit cap", + ); + + throw new BadRequestException( + `UserOp gas limit (${executionGasLimit}) exceeds the maximum executable gas limit (${batchGasLimit}) on chain (${chainId})`, + ); + } + } + const paymasterAndData = isTrustedPaymentUserOp ? "0x" : this.getPaymasterAndData(paymentInfo, isTrustedSponsorship, chainId);