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
State
10-Slot Configuration
Condition Slots (Before Actions)
Condition Slots (Before Actions)
- AUTHORIZE_CONDITION - Who can authorize payments
- CHARGE_CONDITION - Who can charge partial amounts
- RELEASE_CONDITION - Who can release from escrow
- REFUND_IN_ESCROW_CONDITION - Who can refund during escrow
- REFUND_POST_ESCROW_CONDITION - Who can refund after release
address(0) = always allowRecorder Slots (After Actions)
Recorder Slots (After Actions)
- AUTHORIZE_RECORDER - Record authorization (e.g., timestamp)
- CHARGE_RECORDER - Record charge event
- RELEASE_RECORDER - Record release
- REFUND_IN_ESCROW_RECORDER - Record in-escrow refund
- REFUND_POST_ESCROW_RECORDER - Record post-escrow refund
address(0) = no recording (no-op)Key Methods
authorize()
Authorizes a payment and locks funds in escrow.paymentInfo- Payment info struct (must haveoperator == address(this),feeReceiver == address(this))amount- Amount to authorizetokenCollector- Address of the token collector contractcollectorData- Data to pass to the token collector
- Check
AUTHORIZE_CONDITION(if set) - Validate fee bounds compatibility
- Store fees at authorization time (prevents protocol fee changes from breaking capture)
- Call
escrow.authorize() - Call
AUTHORIZE_RECORDER(if set) - Emit
AuthorizationCreated
AUTHORIZE_CONDITION (default: anyone)
charge()
Direct charge - collects payment and immediately transfers to receiver (no escrow hold).paymentInfo- Payment info struct (must haveoperator == address(this),feeReceiver == address(this))amount- Amount to chargetokenCollector- Address of the token collector contractcollectorData- Data to pass to the token collector
- Check
CHARGE_CONDITION(if set) - Validate fee bounds compatibility
- Call
escrow.charge()- funds go directly to receiver - Accumulate protocol fees for later distribution
- Call
CHARGE_RECORDER(if set) - Emit
ChargeExecuted
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).paymentInfo- Payment info structamount- Amount to release
- Check
RELEASE_CONDITION(if set) - Use fees stored at authorization time
- Call
escrow.capture() - Accumulate protocol fees for later distribution
- Call
RELEASE_RECORDER(if set) - Emit
ReleaseExecuted
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).paymentInfo- Payment info structamount- Amount to return to payer
- Check
REFUND_IN_ESCROW_CONDITION(if set) - Call
escrow.partialVoid()to return funds to payer - Call
REFUND_IN_ESCROW_RECORDER(if set) - Emit
RefundInEscrowExecuted
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).paymentInfo- Payment info structamount- Amount to refund to payertokenCollector- Address of the token collector that will source the refundcollectorData- Data to pass to the token collector (e.g., signatures)
- Check
REFUND_POST_ESCROW_CONDITION(if set) - Call
escrow.refund()- token collector enforces permission - Call
REFUND_POST_ESCROW_RECORDER(if set) - Emit
RefundPostEscrowExecuted
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
- 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
authorize() time in authorizedFees[hash]. This prevents protocol fee changes from breaking capture of already-authorized payments.
- 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 viadistributeFees(token):
Protocol Fee Changes (7-day Timelock)
Protocol fee calculator changes require a 7-day timelock onProtocolFeeConfig:
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.
