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: Cannot be paused or upgraded
  • Configuration: 10 slots for conditions and recorders
  • Use Cases: Marketplace, subscriptions, streaming, grants, custom flows

Immutable Fields

address public immutable ESCROW;              // AuthCaptureEscrow address
address public immutable FEE_RECIPIENT;       // 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_CONDITION - Who can authorize payments
  2. CHARGE_CONDITION - Who can charge partial amounts
  3. RELEASE_CONDITION - Who can release from escrow
  4. REFUND_IN_ESCROW_CONDITION - Who can refund during escrow
  5. REFUND_POST_ESCROW_CONDITION - Who can refund after release
Default: address(0) = always allow
  1. AUTHORIZE_RECORDER - Record authorization (e.g., timestamp)
  2. CHARGE_RECORDER - Record charge event
  3. RELEASE_RECORDER - Record release
  4. REFUND_IN_ESCROW_RECORDER - Record in-escrow refund
  5. REFUND_POST_ESCROW_RECORDER - Record post-escrow 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_CONDITION (if set)
  2. Validate fee bounds compatibility
  3. Store fees at authorization time (prevents protocol fee changes from breaking capture)
  4. Call escrow.authorize()
  5. Call AUTHORIZE_RECORDER (if set)
  6. Emit AuthorizationCreated
Access: Controlled by AUTHORIZE_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 allow the payer to reclaim funds after expiry. This is useful for subscription-based payments where you want to limit 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_CONDITION (if set)
  2. Validate fee bounds compatibility
  3. Call escrow.charge() - funds go directly to receiver
  4. Accumulate protocol fees for later distribution
  5. Call CHARGE_RECORDER (if set)
  6. Emit ChargeExecuted
Access: Controlled by CHARGE_CONDITION (default: anyone)
Unlike authorize(), funds go directly to receiver without escrow hold. Refunds are only possible via refundPostEscrow().

release()

Releases funds from escrow to receiver (capture).
function release(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint256 amount
) external nonReentrant
Parameters:
  • paymentInfo - Payment info struct
  • amount - Amount to release
Flow:
  1. Check RELEASE_CONDITION (if set)
  2. Use fees stored at authorization time
  3. Call escrow.capture()
  4. Accumulate protocol fees for later distribution
  5. Call RELEASE_RECORDER (if set)
  6. Emit ReleaseExecuted
Access: Controlled by RELEASE_CONDITION
Marketplace example: Receiver OR StaticAddressCondition(arbiter) + escrow passed Subscription example: StaticAddressCondition(serviceProvider) DAO example: StaticAddressCondition(daoMultisig)

refundInEscrow()

Refunds payment while still in escrow (partial void).
function refundInEscrow(
    AuthCaptureEscrow.PaymentInfo calldata paymentInfo,
    uint120 amount
) external nonReentrant
Parameters:
  • paymentInfo - Payment info struct
  • amount - Amount to return to payer
Flow:
  1. Check REFUND_IN_ESCROW_CONDITION (if set)
  2. Call escrow.partialVoid() to return funds to payer
  3. Call REFUND_IN_ESCROW_RECORDER (if set)
  4. Emit RefundInEscrowExecuted
Access: Controlled by REFUND_IN_ESCROW_CONDITION
Marketplace example: StaticAddressCondition(arbiter) - disputes Return policy example: Receiver OR StaticAddressCondition(arbiter) DAO example: StaticAddressCondition(daoMultisig) Subscription example: address(0) - no refunds

refundPostEscrow()

Refunds payment after it has been released (captured).
function refundPostEscrow(
    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 (e.g., signatures)
Flow:
  1. Check REFUND_POST_ESCROW_CONDITION (if set)
  2. Call escrow.refund() - token collector enforces permission
  3. Call REFUND_POST_ESCROW_RECORDER (if set)
  4. Emit RefundPostEscrowExecuted
Access: Controlled by REFUND_POST_ESCROW_CONDITION. Permission is also enforced by the token collector (e.g., receiver must have approved it, or collectorData contains receiver’s signature).
Marketplace example: StaticAddressCondition(arbiter) - post-delivery disputes Return policy example: Receiver - voluntary returns Most configurations: address(0) - no post-escrow 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_RECIPIENT;  // Operator fee recipient
// Protocol fee recipient is on ProtocolFeeConfig

// Fee tracking for accurate distribution
mapping(address token => uint256) public accumulatedProtocolFees;
Example Fee Calculation (Additive): For a 1000 USDC payment:
  • Protocol Fee: 3 bps (0.03%) = 0.30 USDC -> goes to protocolFeeRecipient
  • Operator Fee: 2 bps (0.02%) = 0.20 USDC -> goes to FEE_RECIPIENT
  • Total Fee: 5 bps (0.05%) = 0.50 USDC
  • Receiver Gets: 999.50 USDC
Fee Locking: Fees are calculated and stored at authorize() time in authorizedFees[hash]. This prevents protocol fee changes from breaking capture of already-authorized payments.
struct AuthorizedFees {
    uint16 totalFeeBps;
    uint16 protocolFeeBps;
}
mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees;
FEE_RECIPIENT 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 and are distributed via distributeFees(token):
// Anyone can call to distribute fees for a token
operator.distributeFees(usdcAddress);
// Protocol share -> protocolFeeRecipient
// Operator share -> FEE_RECIPIENT

Protocol Fee Changes (7-day Timelock)

Protocol fee calculator changes require a 7-day timelock on ProtocolFeeConfig:
// Step 1: Queue new calculator
protocolFeeConfig.queueCalculator(newCalculatorAddress);

// Step 2: Wait 7 days

// Step 3: Execute
protocolFeeConfig.executeCalculator();
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 cannot be changed

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.