Skip to main content

IExecutableModule

Title: IExecutableModule

Author: amser

Canonical execution interface for all executable modules.

This interface allows PaymentProcessor to execute any module type (subscriptions, credits, hybrid, etc.) without understanding module semantics. The processor only enforces:

  • Execution is allowed (quoteExecution returns None)
  • Idempotency (windowId)
  • Funding (Permit2)
  • Transfers Modules define:
  • When execution is due
  • What amount to charge
  • How to update state after execution

Functions

quoteExecution

Quotes whether execution is allowed and returns execution parameters.

Single entry point that replaces canExecute, getExecutionFailureReason, and executionContext. Modules MUST NOT revert for "not found" cases; instead they return the appropriate ExecutionFailureReason. When reason == None, the remaining return values contain valid execution context. When reason != None, context values are zeroed out.

function quoteExecution(uint256 id)
external
view
returns (
ExecutionFailureReason reason,
address payer,
address recipient,
address token,
uint256 amount,
uint256 executionTime,
uint256 windowId
);

Parameters

NameTypeDescription
iduint256Module-specific identifier (e.g., subscription ID, envelope ID)

Returns

NameTypeDescription
reasonExecutionFailureReasonStandardized failure reason (None if executable)
payeraddressAddress that will pay (for Permit2)
recipientaddressAddress that will receive payment
tokenaddressToken address to transfer
amountuint256Amount to transfer
executionTimeuint256Canonical execution timestamp for this window
windowIduint256Idempotency window identifier

onExecute

Called by PaymentProcessor after a successful Permit2 transfer.

IMPORTANT: This function must be gas-efficient. PaymentProcessor does not apply a gas stipend to this call. An expensive onExecute() implementation can cause batchExecute() calls to run out of gas, affecting other IDs in the same batch. Guidelines:

  • Keep onExecute() to storage writes only (no external calls)
  • Avoid loops over unbounded arrays
  • Target less than 50,000 gas for the full onExecute() call If this module is ever opened to third-party implementations, a gas stipend (e.g. onExecute{gas: 100_000}(...)) should be added in PaymentProcessor._executeOne(). Called exactly once per window by PaymentProcessor after successful transfer. Module updates its internal state (e.g., lastPaidAt, nextChargeAt, remainingExecutions).
function onExecute(uint256 id, uint256 executedAt) external;

Parameters

NameTypeDescription
iduint256Module-specific identifier (e.g., subscription ID)
executedAtuint256Timestamp of execution

Enums

ExecutionFailureReason

Standard execution failure reasons.

Modules MUST map their internal failure states to these standard reasons. This ensures the processor can handle failures generically without understanding module-specific semantics. Keepers use these reasons to determine retry behavior:

  • None: Execution is allowed
  • NotFound: Never retry (permanent)
  • Paused: Retry after state change (user unpauses)
  • PlanInactive: Retry after state change (merchant reactivates)
  • NoRemainingExecutions: Never retry (permanent until user tops up)
  • AllowanceExpired: Never retry (permanent until user renews)
  • NotYetDue: Retry after nextChargeAt
  • PaymentWindowViolation: Never retry (requires manual resume)
  • ExecutionNotAllowed: Generic catch-all, check module state
enum ExecutionFailureReason {
None, // 0 - No failure (can execute)
NotFound, // 1 - Entity does not exist
Paused, // 2 - Entity is paused
PlanInactive, // 3 - Plan is not active
NoRemainingExecutions, // 4 - Execution budget exhausted
AllowanceExpired, // 5 - Module-stored allowance expiry passed
NotYetDue, // 6 - Execution not yet due
PaymentWindowViolation, // 7 - Beyond payment window (requires manual reset)
ExecutionNotAllowed // 8 - Catch-all for other cases
}