Skip to content

eth_estimateUserOperationGas uses eip7702Auth without DTO validation #389

Description

@chenshj73

Summary

eth_estimateUserOperationGas appears to support userOp.eip7702Auth in the execution layer, but its request DTO does not declare or validate that field. In contrast, eth_sendUserOperation has an eip7702Auth DTO with nested validation.

This creates a schema drift between the send and estimate RPC paths: callers can pass an EIP-7702 authorization object to estimation, the executor will use it, but malformed or incomplete authorization fields are not rejected by the same request validation layer that protects the send path.

Affected code

packages/api/src/dto/SendUserOperation.dto.ts defines and validates eip7702Auth:

export class Eip7702Auth {
  @IsBigNumberish()
  chainId!: BigNumberish;
  @IsBigNumberish()
  nonce!: BigNumberish;
  @IsEthereumAddress()
  address!: Hex;
  @IsString()
  r!: Hex;
  @IsString()
  s!: Hex;
  yParity!: "0x0" | "0x1";
}

export class SendUserOperation {
  ...
  /**
   * Eip-7702 property
   */
  @IsOptional()
  @ValidateNested()
  @Type(() => Eip7702Auth)
  eip7702Auth?: Eip7702Auth;
}

But packages/api/src/dto/EstimateUserOperation.dto.ts does not include this field:

export class EstimateUserOperation {
  /**
   * Common Properties
   */
  @IsEthereumAddress()
  sender!: Hex;

  @IsBigNumberish()
  nonce!: BigNumberish;

  @IsString()
  callData!: Hex;

  @IsString()
  signature!: Hex;

  /**
   * EntryPoint v7 Properties
   */
  @IsValidFactory()
  @IsOptional()
  factory?: Hex;

  @IsString()
  @IsOptional()
  factoryData?: Hex;

  @IsEthereumAddress()
  @IsOptional()
  paymaster?: Hex;

  @IsBigNumberish()
  @IsOptional()
  paymasterVerificationGasLimit?: BigNumberish;

  @IsBigNumberish()
  @IsOptional()
  paymasterPostOpGasLimit?: BigNumberish;

  @IsString()
  @IsOptional()
  paymasterData?: Hex;
}

The API layer does validate estimation requests with that DTO:

@RpcMethodValidator(EstimateUserOperationGasArgs)
async estimateUserOperationGas(
  args: EstimateUserOperationGasArgs
): Promise<EstimatedUserOperationGas> {
  return await this.ethModule.estimateUserOperationGas(args);
}

However, the executor later reads userOp.eip7702Auth in the estimation path and uses its fields to build an authorizationList:

if (userOp.eip7702Auth && !this.config.eip7702) {
  throw new RpcError(
    "EIP7702 is not supported in this network",
    RpcErrorCodes.INVALID_USEROP
  );
}
...
if (userOp.eip7702Auth) {
  ethEstimateGas = await this.publicClient
    .estimateGas({
      account: entryPoint as `0x${string}`,
      to: userOp.sender as `0x${string}`,
      data: userOp.callData as `0x${string}`,
      authorizationList: [
        {
          address: userOp.eip7702Auth.address,
          chainId: Number(BigInt(userOp.eip7702Auth.chainId)),
          nonce: Number(BigInt(userOp.eip7702Auth.nonce)),
          r: userOp.eip7702Auth.r
            .toString()
            .replace(/^0x0+(?=\d)/, "0x") as `0x${string}`,
          s: userOp.eip7702Auth.s
            .toString()
            .replace(/^0x0+(?=\d)/, "0x") as `0x${string}`,
          yParity: userOp.eip7702Auth.yParity === "0x0" ? 0 : 1,
        },
      ],
    })

I also only found the DTO declaration in the send path during a quick test search:

packages/api/src/dto/SendUserOperation.dto.ts:86:  eip7702Auth?: Eip7702Auth;

Why this matters

For EIP-7702 UserOperations, estimation and submission generally need to agree on the same authorization semantics. If eth_estimateUserOperationGas accepts an eip7702Auth object but does not validate its nested fields, malformed estimation requests can fail later with conversion/runtime errors such as BigInt(...)/.toString() failures, or they may be handled differently from eth_sendUserOperation.

This is not a fund-loss report. It is an RPC/API consistency and hardening issue that can make EIP-7702 integration debugging confusing.

Suggested fix

Consider reusing the same Eip7702Auth DTO in EstimateUserOperation:

export class EstimateUserOperation {
  ...
  @IsOptional()
  @ValidateNested()
  @Type(() => Eip7702Auth)
  eip7702Auth?: Eip7702Auth;
}

It may also be worth validating yParity explicitly, since it currently has a TypeScript literal type but no runtime validator.

Useful regression tests:

  1. eth_estimateUserOperationGas with a well-formed eip7702Auth passes request validation and reaches the EIP-7702 estimation branch.
  2. eth_estimateUserOperationGas with an incomplete/malformed eip7702Auth returns an INVALID_REQUEST-style validation error rather than a later runtime/internal error.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions