From d6d61daf8046f93657eea067287eb1898da0cc4d Mon Sep 17 00:00:00 2001
From: Luis Schaab <schaable@gmail.com>
Date: Thu, 11 Jul 2024 19:55:10 -0300
Subject: [PATCH 1/3] Ensure the argument object provided by the user is not
 modified

---
 .../core/src/internal/tasks/resolved-task.ts  | 45 +++++++++----------
 .../core/test/internal/tasks/task-manager.ts  | 25 +++++++++--
 2 files changed, 41 insertions(+), 29 deletions(-)

diff --git a/v-next/core/src/internal/tasks/resolved-task.ts b/v-next/core/src/internal/tasks/resolved-task.ts
index 61e6c69bd6..351fad9863 100644
--- a/v-next/core/src/internal/tasks/resolved-task.ts
+++ b/v-next/core/src/internal/tasks/resolved-task.ts
@@ -100,34 +100,29 @@ export class ResolvedTask implements Task {
     }
 
     const providedArgumentNames = new Set(Object.keys(taskArguments));
-
-    // Validate and resolve the task options
-    for (const option of this.options.values()) {
-      const value = taskArguments[option.name];
-
-      this.#validateArgumentType(option, value);
-
-      // resolve defaults for optional arguments
-      if (value === undefined) {
-        taskArguments[option.name] = option.defaultValue;
+    const argumentDefinitions = [
+      ...this.options.values(),
+      ...this.positionalArguments,
+    ];
+    const validatedTaskArguments: TaskArguments = {};
+    for (const argumentDefinition of argumentDefinitions) {
+      const value = taskArguments[argumentDefinition.name];
+      const isPositional = "isVariadic" in argumentDefinition;
+
+      if (isPositional) {
+        this.#validateRequiredArgument(argumentDefinition, value);
       }
-
-      providedArgumentNames.delete(option.name);
-    }
-
-    // Validate and resolve the task positional arguments
-    for (const argument of this.positionalArguments) {
-      const value = taskArguments[argument.name];
-
-      this.#validateRequiredArgument(argument, value);
-      this.#validateArgumentType(argument, value, argument.isVariadic);
+      this.#validateArgumentType(
+        argumentDefinition,
+        value,
+        isPositional && argumentDefinition.isVariadic,
+      );
 
       // resolve defaults for optional arguments
-      if (value === undefined && argument.defaultValue !== undefined) {
-        taskArguments[argument.name] = argument.defaultValue;
-      }
+      validatedTaskArguments[argumentDefinition.name] =
+        value ?? argumentDefinition.defaultValue;
 
-      providedArgumentNames.delete(argument.name);
+      providedArgumentNames.delete(argumentDefinition.name);
     }
 
     // At this point, the set should be empty as all the task arguments have
@@ -166,7 +161,7 @@ export class ResolvedTask implements Task {
       );
     };
 
-    return next(taskArguments);
+    return next(validatedTaskArguments);
   }
 
   /**
diff --git a/v-next/core/test/internal/tasks/task-manager.ts b/v-next/core/test/internal/tasks/task-manager.ts
index 595062b5a3..823e44b1f6 100644
--- a/v-next/core/test/internal/tasks/task-manager.ts
+++ b/v-next/core/test/internal/tasks/task-manager.ts
@@ -1102,6 +1102,7 @@ describe("TaskManagerImplementation", () => {
             tasks: [
               new NewTaskDefinitionBuilderImplementation("task1")
                 .addOption({ name: "arg1", defaultValue: "default" })
+                .addOption({ name: "withDefault", defaultValue: "default" })
                 .addFlag({ name: "flag1" })
                 .addPositionalArgument({ name: "posArg" })
                 .addVariadicArgument({ name: "varArg" })
@@ -1111,6 +1112,7 @@ describe("TaskManagerImplementation", () => {
                     flag1: true,
                     posArg: "posValue",
                     varArg: ["varValue1", "varValue2"],
+                    withDefault: "default",
                   });
                 })
                 .build(),
@@ -1132,16 +1134,31 @@ describe("TaskManagerImplementation", () => {
           },
         ],
       });
-
-      const task1 = hre.tasks.getTask("task1");
-      await task1.run({
+      // withDefault option is intentionally omitted
+      const providedArgs = {
         arg1: "arg1Value",
         flag1: true,
         posArg: "posValue",
         varArg: ["varValue1", "varValue2"],
         arg2: "arg2Value",
         flag2: true,
-      });
+      };
+
+      const task1 = hre.tasks.getTask("task1");
+      await task1.run(providedArgs);
+      // Ensure withDefault is not added to the args
+      assert.deepEqual(
+        providedArgs,
+        {
+          arg1: "arg1Value",
+          flag1: true,
+          posArg: "posValue",
+          varArg: ["varValue1", "varValue2"],
+          arg2: "arg2Value",
+          flag2: true,
+        },
+        "Expected the providedArgs to not change",
+      );
     });
 
     it("should run a task with arguments and resolve their default values", async () => {

From 051755d58312b2af0ac3ff7119471aa0f97650ef Mon Sep 17 00:00:00 2001
From: Luis Schaab <schaable@gmail.com>
Date: Thu, 11 Jul 2024 19:55:30 -0300
Subject: [PATCH 2/3] Fix type error

---
 v-next/core/src/types/arguments.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/v-next/core/src/types/arguments.ts b/v-next/core/src/types/arguments.ts
index 231f84b720..b4e69488a2 100644
--- a/v-next/core/src/types/arguments.ts
+++ b/v-next/core/src/types/arguments.ts
@@ -57,7 +57,7 @@ export type ArgumentTypeToValueType<T extends ArgumentType> =
 export interface OptionDefinition<T extends ArgumentType = ArgumentType> {
   name: string;
   description: string;
-  type: ArgumentType;
+  type: T;
   defaultValue: ArgumentTypeToValueType<T>;
 }
 

From 76b414e8315d15ec90f781d87d9b46f0c02ad257 Mon Sep 17 00:00:00 2001
From: Luis Schaab <schaable@gmail.com>
Date: Thu, 11 Jul 2024 19:56:02 -0300
Subject: [PATCH 3/3] Validate plain task object on the task manager

---
 v-next/core/src/internal/tasks/builders.ts    | 197 ++----
 .../core/src/internal/tasks/task-manager.ts   |  42 ++
 v-next/core/src/internal/tasks/utils.ts       |   6 -
 v-next/core/src/internal/tasks/validations.ts | 151 +++++
 .../core/test/internal/tasks/task-manager.ts  | 641 +++++++++++++++++-
 v-next/core/test/internal/tasks/utils.ts      |  17 +-
 6 files changed, 848 insertions(+), 206 deletions(-)
 create mode 100644 v-next/core/src/internal/tasks/validations.ts

diff --git a/v-next/core/src/internal/tasks/builders.ts b/v-next/core/src/internal/tasks/builders.ts
index 62526ac424..f7989acf93 100644
--- a/v-next/core/src/internal/tasks/builders.ts
+++ b/v-next/core/src/internal/tasks/builders.ts
@@ -18,13 +18,14 @@ import { HardhatError } from "@ignored/hardhat-vnext-errors";
 
 import { ArgumentType } from "../../types/arguments.js";
 import { TaskDefinitionType } from "../../types/tasks.js";
-import {
-  RESERVED_ARGUMENT_NAMES,
-  isArgumentValueValid,
-  isArgumentNameValid,
-} from "../arguments.js";
 
-import { formatTaskId, isValidActionUrl } from "./utils.js";
+import { formatTaskId } from "./utils.js";
+import {
+  validateAction,
+  validateId,
+  validateOption,
+  validatePositionalArgument,
+} from "./validations.js";
 
 export class EmptyTaskDefinitionBuilderImplementation
   implements EmptyTaskDefinitionBuilder
@@ -34,11 +35,7 @@ export class EmptyTaskDefinitionBuilderImplementation
   #description: string;
 
   constructor(id: string | string[], description: string = "") {
-    if (id.length === 0) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
-      );
-    }
+    validateId(id);
 
     this.#id = Array.isArray(id) ? id : [id];
     this.#description = description;
@@ -72,11 +69,7 @@ export class NewTaskDefinitionBuilderImplementation
   #action?: NewTaskActionFunction | string;
 
   constructor(id: string | string[], description: string = "") {
-    if (id.length === 0) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
-      );
-    }
+    validateId(id);
 
     this.#id = Array.isArray(id) ? id : [id];
     this.#description = description;
@@ -88,14 +81,7 @@ export class NewTaskDefinitionBuilderImplementation
   }
 
   public setAction(action: NewTaskActionFunction | string): this {
-    if (typeof action === "string" && !isValidActionUrl(action)) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION,
-        {
-          action,
-        },
-      );
-    }
+    validateAction(action);
 
     this.#action = action;
 
@@ -115,45 +101,17 @@ export class NewTaskDefinitionBuilderImplementation
   }): this {
     const argumentType = type ?? ArgumentType.STRING;
 
-    if (!isArgumentNameValid(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, {
-        name,
-      });
-    }
-
-    if (this.#usedNames.has(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, {
-        name,
-      });
-    }
-
-    if (RESERVED_ARGUMENT_NAMES.has(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, {
-        name,
-      });
-    }
-
-    if (!isArgumentValueValid(argumentType, defaultValue)) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
-        {
-          value: defaultValue,
-          name: "defaultValue",
-          type: argumentType,
-          task: formatTaskId(this.#id),
-        },
-      );
-    }
-
-    this.#usedNames.add(name);
-
-    this.#options[name] = {
+    const optionDefinition = {
       name,
       description,
       type: argumentType,
       defaultValue,
     };
 
+    validateOption(optionDefinition, this.#usedNames, this.#id);
+
+    this.#options[name] = optionDefinition;
+
     return this;
   }
 
@@ -223,69 +181,23 @@ export class NewTaskDefinitionBuilderImplementation
   }): this {
     const argumentType = type ?? ArgumentType.STRING;
 
-    if (!isArgumentNameValid(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, {
-        name,
-      });
-    }
-
-    if (this.#usedNames.has(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, {
-        name,
-      });
-    }
-
-    if (RESERVED_ARGUMENT_NAMES.has(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, {
-        name,
-      });
-    }
-
-    if (defaultValue !== undefined) {
-      if (!isArgumentValueValid(argumentType, defaultValue, isVariadic)) {
-        throw new HardhatError(
-          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
-          {
-            value: defaultValue,
-            name: "defaultValue",
-            type: argumentType,
-            task: formatTaskId(this.#id),
-          },
-        );
-      }
-    }
-
-    if (this.#positionalArgs.length > 0) {
-      const lastArg = this.#positionalArgs[this.#positionalArgs.length - 1];
-
-      if (lastArg.isVariadic) {
-        throw new HardhatError(
-          HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC,
-          {
-            name,
-          },
-        );
-      }
-
-      if (lastArg.defaultValue !== undefined && defaultValue === undefined) {
-        throw new HardhatError(
-          HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL,
-          {
-            name,
-          },
-        );
-      }
-    }
-
-    this.#usedNames.add(name);
-
-    this.#positionalArgs.push({
+    const positionalArgDef = {
       name,
       description,
       type: argumentType,
       defaultValue,
       isVariadic,
-    });
+    };
+
+    const lastArg = this.#positionalArgs.at(-1);
+    validatePositionalArgument(
+      positionalArgDef,
+      this.#usedNames,
+      this.#id,
+      lastArg,
+    );
+
+    this.#positionalArgs.push(positionalArgDef);
 
     return this;
   }
@@ -303,11 +215,7 @@ export class TaskOverrideDefinitionBuilderImplementation
   #action?: TaskOverrideActionFunction | string;
 
   constructor(id: string | string[]) {
-    if (id.length === 0) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
-      );
-    }
+    validateId(id);
 
     this.#id = Array.isArray(id) ? id : [id];
   }
@@ -318,14 +226,7 @@ export class TaskOverrideDefinitionBuilderImplementation
   }
 
   public setAction(action: TaskOverrideActionFunction | string): this {
-    if (typeof action === "string" && !isValidActionUrl(action)) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION,
-        {
-          action,
-        },
-      );
-    }
+    validateAction(action);
 
     this.#action = action;
 
@@ -345,43 +246,21 @@ export class TaskOverrideDefinitionBuilderImplementation
   }): this {
     const argumentType = type ?? ArgumentType.STRING;
 
-    if (!isArgumentNameValid(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, {
-        name,
-      });
-    }
-
-    if (name in this.#options) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, {
-        name,
-      });
-    }
-
-    if (RESERVED_ARGUMENT_NAMES.has(name)) {
-      throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, {
-        name,
-      });
-    }
-
-    if (!isArgumentValueValid(argumentType, defaultValue)) {
-      throw new HardhatError(
-        HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
-        {
-          value: defaultValue,
-          name: "defaultValue",
-          type: argumentType,
-          task: formatTaskId(this.#id),
-        },
-      );
-    }
-
-    this.#options[name] = {
+    const optionDefinition = {
       name,
       description,
       type: argumentType,
       defaultValue,
     };
 
+    validateOption(
+      optionDefinition,
+      new Set(Object.keys(this.#options)),
+      this.#id,
+    );
+
+    this.#options[name] = optionDefinition;
+
     return this;
   }
 
diff --git a/v-next/core/src/internal/tasks/task-manager.ts b/v-next/core/src/internal/tasks/task-manager.ts
index 1bace05869..54cd53d29a 100644
--- a/v-next/core/src/internal/tasks/task-manager.ts
+++ b/v-next/core/src/internal/tasks/task-manager.ts
@@ -1,3 +1,4 @@
+import type { PositionalArgumentDefinition } from "../../types/arguments.js";
 import type { GlobalOptionDefinitions } from "../../types/global-options.js";
 import type { HardhatRuntimeEnvironment } from "../../types/hre.js";
 import type {
@@ -17,6 +18,12 @@ import { TaskDefinitionType } from "../../types/tasks.js";
 
 import { ResolvedTask } from "./resolved-task.js";
 import { formatTaskId, getActorFragment } from "./utils.js";
+import {
+  validateAction,
+  validateId,
+  validateOption,
+  validatePositionalArgument,
+} from "./validations.js";
 
 export class TaskManagerImplementation implements TaskManager {
   readonly #hre: HardhatRuntimeEnvironment;
@@ -35,6 +42,7 @@ export class TaskManagerImplementation implements TaskManager {
       }
 
       for (const taskDefinition of plugin.tasks) {
+        this.#validateTaskDefinition(taskDefinition);
         this.#reduceTaskDefinition(
           globalOptionDefinitions,
           taskDefinition,
@@ -45,6 +53,7 @@ export class TaskManagerImplementation implements TaskManager {
 
     // reduce global user defined tasks
     for (const taskDefinition of this.#hre.config.tasks) {
+      this.#validateTaskDefinition(taskDefinition);
       this.#reduceTaskDefinition(globalOptionDefinitions, taskDefinition);
     }
   }
@@ -243,4 +252,37 @@ export class TaskManagerImplementation implements TaskManager {
 
     task.actions.push({ pluginId, action: taskDefinition.action });
   }
+
+  #validateTaskDefinition(taskDefinition: TaskDefinition): void {
+    validateId(taskDefinition.id);
+
+    // Empty tasks don't have actions, options, or positional arguments
+    if (taskDefinition.type === TaskDefinitionType.EMPTY_TASK) {
+      return;
+    }
+
+    const usedNames = new Set<string>();
+
+    validateAction(taskDefinition.action);
+
+    Object.values(taskDefinition.options).forEach((optionDefinition) =>
+      validateOption(optionDefinition, usedNames, taskDefinition.id),
+    );
+
+    // Override tasks don't have positional arguments
+    if (taskDefinition.type === TaskDefinitionType.TASK_OVERRIDE) {
+      return;
+    }
+
+    let lastArg: PositionalArgumentDefinition;
+    taskDefinition.positionalArguments.forEach((posArgDefinition) => {
+      validatePositionalArgument(
+        posArgDefinition,
+        usedNames,
+        taskDefinition.id,
+        lastArg,
+      );
+      lastArg = posArgDefinition;
+    });
+  }
 }
diff --git a/v-next/core/src/internal/tasks/utils.ts b/v-next/core/src/internal/tasks/utils.ts
index 00c67aea0c..3d35304594 100644
--- a/v-next/core/src/internal/tasks/utils.ts
+++ b/v-next/core/src/internal/tasks/utils.ts
@@ -9,9 +9,3 @@ export function formatTaskId(taskId: string | string[]): string {
 export function getActorFragment(pluginId: string | undefined): string {
   return pluginId !== undefined ? `Plugin ${pluginId} is` : "You are";
 }
-
-const FILE_PROTOCOL_PATTERN = /^file:\/\/.+/;
-
-export function isValidActionUrl(action: string): boolean {
-  return FILE_PROTOCOL_PATTERN.test(action);
-}
diff --git a/v-next/core/src/internal/tasks/validations.ts b/v-next/core/src/internal/tasks/validations.ts
new file mode 100644
index 0000000000..4973f96c1f
--- /dev/null
+++ b/v-next/core/src/internal/tasks/validations.ts
@@ -0,0 +1,151 @@
+import type {
+  ArgumentType,
+  ArgumentValue,
+  OptionDefinition,
+  PositionalArgumentDefinition,
+} from "../../types/arguments.js";
+
+import { HardhatError } from "@ignored/hardhat-vnext-errors";
+
+import {
+  isArgumentNameValid,
+  isArgumentValueValid,
+  RESERVED_ARGUMENT_NAMES,
+} from "../arguments.js";
+
+import { formatTaskId } from "./utils.js";
+
+export function validateId(id: string | string[]): void {
+  if (id.length === 0) {
+    throw new HardhatError(HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID);
+  }
+}
+
+export function validateAction(action: unknown): void {
+  if (typeof action === "string" && !isValidActionUrl(action)) {
+    throw new HardhatError(
+      HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION,
+      {
+        action,
+      },
+    );
+  }
+}
+
+export function validateOption(
+  optionDefinition: OptionDefinition,
+  usedNames: Set<string>,
+  taskId: string | string[],
+): void {
+  validateArgumentName(optionDefinition.name);
+
+  if (usedNames.has(optionDefinition.name)) {
+    throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, {
+      name: optionDefinition.name,
+    });
+  }
+
+  validateArgumentValue({
+    name: "defaultValue",
+    value: optionDefinition.defaultValue,
+    expectedType: optionDefinition.type,
+    taskId: formatTaskId(taskId),
+  });
+
+  usedNames.add(optionDefinition.name);
+}
+
+export function validatePositionalArgument(
+  positionalArgDef: PositionalArgumentDefinition,
+  usedNames: Set<string>,
+  taskId: string | string[],
+  lastArg?: PositionalArgumentDefinition,
+): void {
+  validateArgumentName(positionalArgDef.name);
+
+  if (usedNames.has(positionalArgDef.name)) {
+    throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, {
+      name: positionalArgDef.name,
+    });
+  }
+
+  if (positionalArgDef.defaultValue !== undefined) {
+    validateArgumentValue({
+      name: "defaultValue",
+      value: positionalArgDef.defaultValue,
+      isVariadic: positionalArgDef.isVariadic,
+      expectedType: positionalArgDef.type,
+      taskId: formatTaskId(taskId),
+    });
+  }
+
+  if (lastArg !== undefined && lastArg.isVariadic) {
+    throw new HardhatError(
+      HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC,
+      {
+        name: positionalArgDef.name,
+      },
+    );
+  }
+
+  if (
+    lastArg !== undefined &&
+    lastArg.defaultValue !== undefined &&
+    positionalArgDef.defaultValue === undefined
+  ) {
+    throw new HardhatError(
+      HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL,
+      {
+        name: positionalArgDef.name,
+      },
+    );
+  }
+
+  usedNames.add(positionalArgDef.name);
+}
+
+const FILE_PROTOCOL_PATTERN = /^file:\/\/.+/;
+
+function isValidActionUrl(action: string): boolean {
+  return FILE_PROTOCOL_PATTERN.test(action);
+}
+
+function validateArgumentName(name: string): void {
+  if (!isArgumentNameValid(name)) {
+    throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, {
+      name,
+    });
+  }
+
+  if (RESERVED_ARGUMENT_NAMES.has(name)) {
+    throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, {
+      name,
+    });
+  }
+}
+
+function validateArgumentValue({
+  name,
+  expectedType,
+  isVariadic = false,
+  value,
+  taskId,
+}: {
+  name: string;
+  expectedType: ArgumentType;
+  isVariadic?: boolean;
+  value: ArgumentValue | ArgumentValue[];
+  taskId: string | string[];
+}): void {
+  if (!isArgumentValueValid(expectedType, value, isVariadic)) {
+    throw new HardhatError(
+      HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
+      {
+        value,
+        name,
+        type: expectedType,
+        task: formatTaskId(taskId),
+      },
+    );
+  }
+}
diff --git a/v-next/core/test/internal/tasks/task-manager.ts b/v-next/core/test/internal/tasks/task-manager.ts
index 823e44b1f6..ffe46625d5 100644
--- a/v-next/core/test/internal/tasks/task-manager.ts
+++ b/v-next/core/test/internal/tasks/task-manager.ts
@@ -9,6 +9,7 @@ import {
 
 import { ArgumentType, globalOption } from "../../../src/config.js";
 import { createBaseHardhatRuntimeEnvironment } from "../../../src/index.js";
+import { RESERVED_ARGUMENT_NAMES } from "../../../src/internal/arguments.js";
 import {
   EmptyTaskDefinitionBuilderImplementation,
   NewTaskDefinitionBuilderImplementation,
@@ -538,31 +539,6 @@ describe("TaskManagerImplementation", () => {
     });
 
     it("should throw if trying to override a task that doesn't exist", async () => {
-      // Empty id task will not be found as empty ids are not allowed
-      await assertRejectsWithHardhatError(
-        createBaseHardhatRuntimeEnvironment({
-          plugins: [
-            {
-              id: "plugin1",
-              tasks: [
-                // Manually creating a task as the builder doesn't allow empty ids
-                {
-                  type: TaskDefinitionType.TASK_OVERRIDE,
-                  id: [], // empty id
-                  description: "",
-                  action: () => {},
-                  options: {},
-                },
-              ],
-            },
-          ],
-        }),
-        HardhatError.ERRORS.TASK_DEFINITIONS.TASK_NOT_FOUND,
-        {
-          task: "",
-        },
-      );
-
       // task1 will not be found as it's not defined
       await assertRejectsWithHardhatError(
         createBaseHardhatRuntimeEnvironment({
@@ -883,6 +859,621 @@ describe("TaskManagerImplementation", () => {
         },
       );
     });
+
+    describe("plain object validations", () => {
+      it("should throw if the task definition object has an empty id", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.EMPTY_TASK,
+                    id: [],
+                    description: "",
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
+          {},
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: [],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
+          {},
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.TASK_OVERRIDE,
+                    id: [],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID,
+          {},
+        );
+      });
+
+      it("should throw if the task definition object has an invalid action file URL", async () => {
+        const invalidActionFileUrl = "not-a-valid-file-url";
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: invalidActionFileUrl,
+                    options: {},
+                    positionalArguments: [],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION,
+          {
+            action: invalidActionFileUrl,
+          },
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.TASK_OVERRIDE,
+                    id: ["task-id"],
+                    description: "",
+                    action: invalidActionFileUrl,
+                    options: {},
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION,
+          {
+            action: invalidActionFileUrl,
+          },
+        );
+      });
+
+      it("should throw if the task definition object has an option with an invalid name", async () => {
+        const invalidName = "invalid-name";
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {
+                      [invalidName]: {
+                        name: invalidName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        defaultValue: "default",
+                      },
+                    },
+                    positionalArguments: [],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.ARGUMENTS.INVALID_NAME,
+          {
+            name: invalidName,
+          },
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.TASK_OVERRIDE,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {
+                      [invalidName]: {
+                        name: invalidName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        defaultValue: "default",
+                      },
+                    },
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.ARGUMENTS.INVALID_NAME,
+          {
+            name: invalidName,
+          },
+        );
+      });
+
+      it("should throw if the task definition object has an option with an reserved name", async () => {
+        RESERVED_ARGUMENT_NAMES.forEach(async (reservedName) => {
+          await assertRejectsWithHardhatError(
+            createBaseHardhatRuntimeEnvironment({
+              plugins: [
+                {
+                  id: "plugin1",
+                  tasks: [
+                    {
+                      type: TaskDefinitionType.NEW_TASK,
+                      id: ["task-id"],
+                      description: "",
+                      action: () => {},
+                      options: {
+                        [reservedName]: {
+                          name: reservedName,
+                          description: "A description",
+                          type: ArgumentType.STRING,
+                          defaultValue: "default",
+                        },
+                      },
+                      positionalArguments: [],
+                    },
+                  ],
+                },
+              ],
+            }),
+            HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME,
+            {
+              name: reservedName,
+            },
+          );
+
+          await assertRejectsWithHardhatError(
+            createBaseHardhatRuntimeEnvironment({
+              plugins: [
+                {
+                  id: "plugin1",
+                  tasks: [
+                    {
+                      type: TaskDefinitionType.TASK_OVERRIDE,
+                      id: ["task-id"],
+                      description: "",
+                      action: () => {},
+                      options: {
+                        [reservedName]: {
+                          name: reservedName,
+                          description: "A description",
+                          type: ArgumentType.STRING,
+                          defaultValue: "default",
+                        },
+                      },
+                    },
+                  ],
+                },
+              ],
+            }),
+            HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME,
+            {
+              name: reservedName,
+            },
+          );
+        });
+      });
+
+      it("should throw if the task definition object has arguments with an duplicated name", async () => {
+        const duplicatedName = "duplicatedName";
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {
+                      [duplicatedName]: {
+                        name: duplicatedName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        defaultValue: "default",
+                      },
+                    },
+                    positionalArguments: [
+                      {
+                        name: duplicatedName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME,
+          {
+            name: duplicatedName,
+          },
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: duplicatedName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                      {
+                        name: duplicatedName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME,
+          {
+            name: duplicatedName,
+          },
+        );
+      });
+
+      it("should throw if the task definition object has an option with an invalid type for it's default value", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {
+                      arg: {
+                        name: "optionName",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        defaultValue: 1,
+                      },
+                    },
+                    positionalArguments: [],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
+          {
+            value: 1,
+            name: "defaultValue",
+            type: ArgumentType.STRING,
+            task: "task-id",
+          },
+        );
+
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.TASK_OVERRIDE,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {
+                      arg: {
+                        name: "optionName",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        defaultValue: 1,
+                      },
+                    },
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
+          {
+            value: 1,
+            name: "defaultValue",
+            type: ArgumentType.STRING,
+            task: "task-id",
+          },
+        );
+      });
+
+      it("should throw if the task definition object has a positional argument with an invalid name", async () => {
+        const invalidName = "invalid-name";
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: invalidName,
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.ARGUMENTS.INVALID_NAME,
+          {
+            name: invalidName,
+          },
+        );
+      });
+
+      it("should throw if the task definition object has a positional argument with an reserved name", async () => {
+        RESERVED_ARGUMENT_NAMES.forEach(async (reservedName) => {
+          await assertRejectsWithHardhatError(
+            createBaseHardhatRuntimeEnvironment({
+              plugins: [
+                {
+                  id: "plugin1",
+                  tasks: [
+                    {
+                      type: TaskDefinitionType.NEW_TASK,
+                      id: ["task-id"],
+                      description: "",
+                      action: () => {},
+                      options: {},
+                      positionalArguments: [
+                        {
+                          name: reservedName,
+                          description: "A description",
+                          type: ArgumentType.STRING,
+                          isVariadic: false,
+                        },
+                      ],
+                    },
+                  ],
+                },
+              ],
+            }),
+            HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME,
+            {
+              name: reservedName,
+            },
+          );
+        });
+      });
+
+      it("should throw if the task definition object has a positional argument with an invalid type for it's default value", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: "posArg",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                        defaultValue: 1,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
+          {
+            value: 1,
+            name: "defaultValue",
+            type: ArgumentType.STRING,
+            task: "task-id",
+          },
+        );
+      });
+
+      it("should throw if the task definition object has a positional variadic argument with an invalid type for it's default value", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: "posArg",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: true,
+                        defaultValue: [1],
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
+          {
+            value: [1],
+            name: "defaultValue",
+            type: ArgumentType.STRING,
+            task: "task-id",
+          },
+        );
+      });
+
+      it("should throw if the task definition object has a positional argument after a variadic argument", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: "posArg",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: true,
+                        defaultValue: ["default"],
+                      },
+                      {
+                        name: "posArg2",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC,
+          {
+            name: "posArg2",
+          },
+        );
+      });
+
+      it("should throw if the task definition object has a required positional argument after an optional argument", async () => {
+        await assertRejectsWithHardhatError(
+          createBaseHardhatRuntimeEnvironment({
+            plugins: [
+              {
+                id: "plugin1",
+                tasks: [
+                  {
+                    type: TaskDefinitionType.NEW_TASK,
+                    id: ["task-id"],
+                    description: "",
+                    action: () => {},
+                    options: {},
+                    positionalArguments: [
+                      {
+                        name: "posArg",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                        defaultValue: "default",
+                      },
+                      {
+                        name: "posArg2",
+                        description: "A description",
+                        type: ArgumentType.STRING,
+                        isVariadic: false,
+                      },
+                    ],
+                  },
+                ],
+              },
+            ],
+          }),
+          HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL,
+          {
+            name: "posArg2",
+          },
+        );
+      });
+    });
   });
 
   describe("getTask", () => {
diff --git a/v-next/core/test/internal/tasks/utils.ts b/v-next/core/test/internal/tasks/utils.ts
index 20ce34fa16..5b1bf7afc5 100644
--- a/v-next/core/test/internal/tasks/utils.ts
+++ b/v-next/core/test/internal/tasks/utils.ts
@@ -1,10 +1,7 @@
 import assert from "node:assert/strict";
 import { describe, it } from "node:test";
 
-import {
-  formatTaskId,
-  isValidActionUrl,
-} from "../../../src/internal/tasks/utils.js";
+import { formatTaskId } from "../../../src/internal/tasks/utils.js";
 
 describe("Task utils", () => {
   describe("formatTaskId", () => {
@@ -16,16 +13,4 @@ describe("Task utils", () => {
       assert.equal(formatTaskId(["foo", "bar"]), "foo bar");
     });
   });
-
-  describe("isValidActionUrl", () => {
-    it("should return true if the action is a file URL", () => {
-      assert.equal(isValidActionUrl("file://foo"), true);
-    });
-
-    it("should return false if the action is not a file URL", () => {
-      assert.equal(isValidActionUrl("http://foo"), false);
-      assert.equal(isValidActionUrl("file://"), false);
-      assert.equal(isValidActionUrl("missing-protocol"), false);
-    });
-  });
 });