Skip to main content

Task Execution Environment Interface (ITaskExecutionEnvironment)

The ITaskExecutionEnvironment interface defines a standard contract structure for implementing the logic of tasks scheduled via the ITaskManager.

When a task is scheduled, the TaskManager deploys a minimal, task-specific proxy contract (mimic). During execution, the TaskManager calls this proxy contract. The proxy contract then delegatecalls the executeTask function on the user-provided implementation address (which should adhere to this ITaskExecutionEnvironment interface).

Using a dedicated environment contract (implementation) provides enhanced security and allows for complex logic, while the proxy ensures execution context isolation.

Opinionated Interface

This specific interface (executeTask(bytes calldata taskData)) is a common pattern. However, the underlying proxy mechanism can technically delegatecall any function on the implementation contract, provided the taskCallData matches the target function signature and arguments. This interface standardizes the entry point.

Functions

executeTask

function executeTask(
bytes calldata taskData
) external returns (bool success);

The primary function delegatecalled by the task-specific proxy when executing a task associated with this environment implementation.

  • The logic inside this function defines what the task actually does.
  • It typically involves decoding taskData to potentially get further target contract addresses and calldata, and then making calls to those targets.
  • Runs in the storage context of the task-specific proxy, not the TaskManager or the environment contract (implementation) itself.
  • To reschedule the task, the function must explicitly call TaskManager.rescheduleTask(). The return value does not control rescheduling behavior.
Rescheduling from Environment

If a task needs to retry or continue in a subsequent block, the logic within executeTask can call the TaskManager.rescheduleTask function. This will cancel the current task and schedule a new one with the same implementation and taskCallData. Important: Rescheduling only works with bonded shMONAD (not with direct MON payments) because contracts typically can't hold or transfer native MON. See Recurring Tasks for more details.

Implementation Example

Here's a basic example of an environment contract that decodes taskData containing a target address and calldata, then executes the call.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ITaskExecutionEnvironment} from "./ITaskExecutionEnvironment.sol"; // Assume interface is defined

contract BasicTaskEnvironment is ITaskExecutionEnvironment {
// Address of the TaskManager contract that will call this environment.
// Used here for an optional access control modifier, though not strictly
// required due to TaskManager's internal proxy patterns.
address public immutable TASK_MANAGER;

event TaskExecuted(address indexed target, bytes data, bool success);

constructor(address taskManager_) {
TASK_MANAGER = taskManager_;
}

// Optional modifier: Restricts calls to only the TaskManager.
// While TaskManager's proxy ensures only it forwards calls,
// this adds an explicit check within the environment code.
modifier onlyTaskManager() {
require(msg.sender == TASK_MANAGER, "BasicTaskEnvironment: Caller is not the TaskManager");
_;
}

/**
* @notice Executes a task by decoding target and calldata, then calling the target.
* @dev Called via delegatecall by the TaskManager.
* @param taskData Abi-encoded tuple (address target, bytes memory data).
* @return success True if the call to the target succeeded, false otherwise.
*/
function executeTask(bytes calldata taskData)
external
override
// onlyTaskManager // Uncomment if using the modifier
returns (bool success)
{
// Decode the target address and the final calldata from taskData
(address target, bytes memory data) = abi.decode(
taskData,
(address, bytes)
);

// Execute the actual task logic by calling the target contract
(success, ) = target.call(data);

emit TaskExecuted(target, data, success);

// No explicit state clearing needed here as delegatecall context is transient per task,
// but complex environments should manage their state carefully.

return success;
// Note: Reverts within the target.call() will bubble up and cause the
// TaskManager to mark the task execution as failed unless handled here.
}

// --- Optional: Helper function for encoding taskData off-chain ---
// This function is not part of the interface but useful for dApp integration.
function encodeTaskData(address target, bytes memory data)
external
pure
returns (bytes memory)
{
return abi.encode(target, data);
}
}

Usage Example

When scheduling a task that uses an ITaskExecutionEnvironment like the BasicTaskEnvironment above:

  1. Deploy the Environment: First, deploy your environment contract instance (e.g., BasicTaskEnvironment).

    // Example using Foundry's forge create or similar deployment tool
    BasicTaskEnvironment env = new BasicTaskEnvironment(taskManagerAddress);
    address envAddress = address(env);
  2. Encode the Target Call: Prepare the function call you ultimately want the task to execute on your final target contract.

    // Example using ethers.js
    const targetInterface = new ethers.Interface(ITargetAbi);
    const targetCalldata = targetInterface.encodeFunctionData("someFunction", [param1, param2]);
    const targetAddress = "0xYourTargetContractAddress...";
  3. Encode the taskData for the Environment: Pack the targetAddress and targetCalldata according to your environment's requirements.

    // Example using ethers.js AbiCoder
    const abiCoder = ethers.AbiCoder.defaultAbiCoder();
    const packedTaskData = abiCoder.encode(
    ["address", "bytes"],
    [targetAddress, targetCalldata]
    );
  4. Schedule the Task: Call scheduleTask or scheduleWithBond on the TaskManager.

    // Example using ethers.js
    const tx = await taskManager.scheduleTask(
    envAddress, // The deployed environment address
    100_000, // Gas limit for the task
    targetBlock, // Desired execution block
    maxPayment, // Calculated max payment/bond
    packedTaskData, // The data for the environment's executeTask
    { value: paymentAmount } // If paying with native MON
    );

Security Considerations

When implementing an execution environment:

  1. Input Validation

    • Validate all inputs before execution
    • Ensure target addresses are valid
    • Verify calldata format and parameters
  2. State Management

    • Avoid storing persistent state between executions in the proxy context
    • Clear any temporary state after execution
    • Remember that state is stored in the task-specific proxy context
    • Store persistent state in external contracts separate from the execution environment
    • Be aware that the execution environment address (proxy) is only known after scheduling a task
    • Each task generates a new proxy if the calldata or task nonce is different
    • Design your external state contracts with proper access controls
  3. Error Handling

    • Properly handle and propagate errors
    • Consider implementing retry logic with rescheduleTask for failed tasks
  4. Gas Management

    • Be mindful of gas usage in custom logic
    • Respect task size categories (Small ≤ 100,000 gas, Medium ≤ 250,000 gas, Large ≤ 750,000 gas)
  5. Access Control

    • Restrict access to the TaskManager address in your implementation contract
    • Use the onlyTaskManager modifier pattern for sensitive functions
    • Remember that while the execution environment is called via the proxy, msg.sender within the delegatecall context will be the TaskManager
note

While many execution environments implement the onlyTaskManager modifier, it's not strictly required. The Task Manager uses a specialized proxy pattern that automatically enforces that only the Task Manager can forward calls to the execution environment.

State Management Architecture

For tasks that need to maintain state across executions (like recurring tasks with configurable intervals):

┌───────────────────┐      ┌───────────────────┐      ┌───────────────────┐
│ │ │ │ │ │
│ Task Proxy #1 │──┐ │ Task Proxy #2 │──┐ │ Task Proxy #3 │──┐
│ (Task Execution) │ │ │ (Task Execution) │ │ │ (Task Execution) │ │
│ │ │ │ │ │ │ │ │
└───────────────────┘ │ └───────────────────┘ │ └───────────────────┘ │
│ │ │
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────────────────────┐
│ │
│ External State Contract (Shared Storage) │
│ │
└───────────────────────────────────────────────────────────┘

This pattern allows multiple task executions to read from and write to the same state, even though each execution runs in an isolated proxy context.

See Also