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

address public immutable ESCROW;              // AuthCaptureEscrow address
address public immutable FEE_RECEIVER;       // Operator fee recipient
ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG; // Shared protocol fee config
IFeeCalculator public immutable FEE_CALCULATOR; // Operator fee calculator

State

// Fee tracking for accurate distribution
mapping(address token => uint256) public accumulatedProtocolFees;

// Fees locked at authorization time
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;

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.
function authorize(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint256 amount,
    address tokenCollector,
    bytes calldata collectorData
) external nonReentrant
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).
function charge(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint256 amount,
    address tokenCollector,
    bytes calldata collectorData
) external nonReentrant
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).
function capture(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint256 amount,
    bytes calldata data
) external nonReentrant
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.
function void(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    bytes calldata data
) external nonReentrant
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).
function refund(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint256 amount,
    address tokenCollector,
    bytes calldata collectorData
) external nonReentrant
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

// Shared protocol fee config (timelocked, swappable calculator)
ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG;

// Per-operator fee calculator (immutable, set at deploy)
IFeeCalculator public immutable FEE_CALCULATOR;

// Fee recipients
address public immutable FEE_RECEIVER;  // Operator fee recipient
// Protocol fee recipient is on ProtocolFeeConfig

// Fee tracking for accurate distribution
mapping(address token => uint256) public accumulatedProtocolFees;
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.
struct AuthorizedFees {
    uint16 totalFeeBps;
    uint16 protocolFeeBps;
}
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
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:
// Anyone can call to distribute fees for a token
operator.distributeFees(usdcAddress);
// Protocol share -> protocolFeeRecipient
// Operator share -> FEE_RECEIVER

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.