Skip to main content

Overview

The main payment operator contract with pluggable conditions for flexible authorization logic.
  • Type: Operator instance (one per fee recipient + configuration)
  • Deployment: Via PaymentOperatorFactory
  • Immutability: No pause switch, no upgrade path
  • Configuration: 10 slots for conditions and hooks
  • Use Cases: Marketplace, subscriptions, streaming, grants, custom flows

Immutable Fields

State

10-Slot Configuration

  1. AUTHORIZE_PRE_ACTION_CONDITION - Who can authorize payments
  2. CHARGE_PRE_ACTION_CONDITION - Who can charge partial amounts
  3. CAPTURE_PRE_ACTION_CONDITION - Who can capture funds from escrow
  4. VOID_PRE_ACTION_CONDITION - Who can refund during escrow
  5. REFUND_PRE_ACTION_CONDITION - Who can refund after capture
Default: address(0) = always allow
  1. AUTHORIZE_POST_ACTION_HOOK - Record authorization (for example, timestamp)
  2. CHARGE_POST_ACTION_HOOK - Record charge event
  3. CAPTURE_POST_ACTION_HOOK - Record capture
  4. VOID_POST_ACTION_HOOK - Record void
  5. REFUND_POST_ACTION_HOOK - Record refund
Default: address(0) = no recording (no-op)

Key Methods

authorize()

Authorizes a payment and locks funds in escrow.
Parameters:
  • paymentInfo - Payment info struct (must have operator == address(this), feeReceiver == address(this))
  • amount - Amount to authorize
  • tokenCollector - Address of the token collector contract
  • collectorData - Data to pass to the token collector
Flow:
  1. Check AUTHORIZE_PRE_ACTION_CONDITION (if set)
  2. Check fee bounds compatibility
  3. Store fees at authorization time (prevents protocol fee changes from breaking capture)
  4. Call escrow.authorize()
  5. Call AUTHORIZE_POST_ACTION_HOOK (if set)
  6. Emit AuthorizeExecuted
Access: Controlled by AUTHORIZE_PRE_ACTION_CONDITION (default: anyone)
Authorization Expiry: The PaymentInfo struct includes an authorizationExpiry field (from base commerce-payments). Set this to type(uint48).max for no expiry, or specify a timestamp to let the payer reclaim funds after expiry. Subscription-based payments use this to bound the authorization window.

charge()

Direct charge - collects payment and immediately transfers to receiver (no escrow hold).
Parameters:
  • paymentInfo - Payment info struct (must have operator == address(this), feeReceiver == address(this))
  • amount - Amount to charge
  • tokenCollector - Address of the token collector contract
  • collectorData - Data to pass to the token collector
Flow:
  1. Check CHARGE_PRE_ACTION_CONDITION (if set)
  2. Check fee bounds compatibility
  3. Call escrow.charge() - funds go directly to receiver
  4. Accumulate protocol fees for later distribution
  5. Call CHARGE_POST_ACTION_HOOK (if set)
  6. Emit ChargeExecuted
Access: Controlled by CHARGE_PRE_ACTION_CONDITION (default: anyone)
Unlike authorize(), funds go directly to receiver without escrow hold. Refunds are only possible via refund().

capture()

Releases funds from escrow to receiver (capture).
Parameters:
  • paymentInfo - Payment info struct
  • amount - Amount to capture
  • data - Optional pass-through data for the pre/post action plugins
Flow:
  1. Check CAPTURE_PRE_ACTION_CONDITION (if set)
  2. Use fees stored at authorization time
  3. Call escrow.capture()
  4. Accumulate protocol fees for later distribution
  5. Call CAPTURE_POST_ACTION_HOOK (if set)
  6. Emit CaptureExecuted
Access: Controlled by CAPTURE_PRE_ACTION_CONDITION
Marketplace example: Receiver OR StaticAddressCondition(arbiter) + escrow passed Subscription example: StaticAddressCondition(serviceProvider) DAO example: StaticAddressCondition(daoMultisig)

void()

Returns all escrowed funds to the payer before capture. Full-only: escrow.void() empties the authorization in one transaction.
Parameters:
  • paymentInfo - Payment info struct
  • data - Optional pass-through data for the pre/post action plugins
Flow:
  1. Check VOID_PRE_ACTION_CONDITION (if set)
  2. Call escrow.void() to return escrowed funds to payer
  3. Call VOID_POST_ACTION_HOOK (if set)
  4. Emit VoidExecuted
Access: Controlled by VOID_PRE_ACTION_CONDITION
Marketplace example: StaticAddressCondition(arbiter) for disputes Return policy example: Receiver OR StaticAddressCondition(arbiter) DAO example: StaticAddressCondition(daoMultisig) Subscription example: address(0), no voids allowed
For a partial return, call capture() for the amount to keep. Then void() the unused authorization, or let the payer reclaim it after captureDeadline.

refund()

Refunds a payment after capture (after the receiver has the funds).
Parameters:
  • paymentInfo - Payment info struct
  • amount - Amount to refund to payer
  • tokenCollector - Address of the token collector that will source the refund
  • collectorData - Data to pass to the token collector (for example, signatures)
Flow:
  1. Check REFUND_PRE_ACTION_CONDITION (if set)
  2. Call escrow.refund() - token collector enforces permission
  3. Call REFUND_POST_ACTION_HOOK (if set)
  4. Emit RefundExecuted
Access: Controlled by REFUND_PRE_ACTION_CONDITION. The token collector also enforces permission (for example, the receiver must have approved it, or collectorData contains the receiver’s signature).
Marketplace example: StaticAddressCondition(arbiter) - post-delivery disputes Return policy example: Receiver - voluntary returns Most configurations: address(0) - no refunds

Fee System (Modular, Additive)

Fees are additive and modular: totalFee = protocolFee + operatorFee

Fee Architecture

Fees are additive: totalFee = protocolFee + operatorFee, split between protocolFeeRecipient and the operator’s FEE_RECEIVER. For a worked example with concrete amounts, see the Fee System. Fee Locking: The operator calculates fees at authorize() time and stores them in authorizedFees[hash]. This stops later protocol fee changes from breaking capture of already-authorized payments.
FEE_RECEIVER can be:
  • Arbiter (marketplace with disputes)
  • Service Provider (subscriptions)
  • Platform Treasury (platform-controlled)
  • DAO Multisig (governance-controlled)

Fee Distribution

Fees accumulate in the operator contract. Call distributeFees(token) to disburse them:

Protocol Fee Changes (7-day Timelock)

Protocol fee calculator and recipient changes require a 7-day timelock on ProtocolFeeConfig. See Fee System: 7-day timelock for the full queue, wait, execute workflow.
Protocol fee changes require 7-day timelock. Operator fees are immutable (set at deploy time).

Security Features

  • ReentrancyGuardTransient - EIP-1153 transient storage for gas-efficient reentrancy protection
  • Ownership - Solady’s Ownable with 2-step transfer
  • Timelock - 7-day delay on protocol fee changes (operator fees are immutable)
  • Immutable Core - Escrow, conditions, and fee configuration stay fixed after deployment

Next Steps

Periphery Contracts

AuthCaptureEscrow, RefundRequest, and other supporting contracts.

Conditions

Explore the pluggable condition system.

Factories

Deploy operators with factory patterns.

Examples

See real-world configuration examples.