Skip to content

BackTrackCo/x402r-contracts

Repository files navigation

x402r Contracts

CI codecov

Deployed Contracts

⚠️ WARNING: CONTRACTS UNAUDITED - USE AT YOUR OWN RISK

These contracts have been deployed but have not been audited. Users should exercise extreme caution and conduct thorough due diligence before interacting with these contracts. The developers assume no liability for any losses incurred from using these contracts.

Documentation

For auditors and developers:

Document Description
AUDIT_PREP.md 📦 Complete audit preparation package (beta release)
SECURITY_REVIEW.md 📋 Comprehensive security review documentation
SECURITY.md 🔒 Security overview and threat model
OPERATOR_SECURITY.md 🛡️ Operator-specific security considerations
TOKENS.md 🪙 Token compatibility and handling
FUZZING.md 🔬 Fuzzing methodology and invariants
DEPLOYMENT.md 🚀 Contract deployment guide
DEPLOYMENT_CHECKLIST.md ✅ Production deployment checklist
MONITORING.md 📈 Event monitoring and indexing
GAS_BREAKDOWN.md 📊 Detailed gas cost analysis

Quick Start

# Clone and build
git clone --recursive https://github.com/x402r/x402r-contracts.git
cd x402r-contracts
forge build

# Run tests
forge test

# Check formatting
forge fmt --check

Deploy a Payment Operator (Local)

import {AuthCaptureEscrow} from "commerce-payments/AuthCaptureEscrow.sol";
import {ProtocolFeeConfig} from "src/plugins/fees/ProtocolFeeConfig.sol";
import {PaymentOperatorFactory} from "src/operator/PaymentOperatorFactory.sol";
import {PaymentOperator} from "src/operator/payment/PaymentOperator.sol";

// 1. Deploy infrastructure
AuthCaptureEscrow escrow = new AuthCaptureEscrow();
ProtocolFeeConfig feeConfig = new ProtocolFeeConfig(address(0), feeReceiver, owner);
PaymentOperatorFactory factory = new PaymentOperatorFactory(address(escrow), address(feeConfig));

// 2. Configure and deploy operator
PaymentOperatorFactory.OperatorConfig memory config = PaymentOperatorFactory.OperatorConfig({
    feeReceiver: feeReceiver,
    feeCalculator: address(0),            // No operator fee
    authorizePreActionCondition: address(0),        // Anyone can authorize
    authorizePostActionHook: address(0),         // No post-action hook
    chargePreActionCondition: address(0),
    chargePostActionHook: address(0),
    capturePreActionCondition: address(0),          // Anyone can capture
    capturePostActionHook: address(0),
    voidPreActionCondition: address(0),
    voidPostActionHook: address(0),
    refundPreActionCondition: address(0),
    refundPostActionHook: address(0)
});
address operator = factory.deployOperator(config);

// 3. Use the operator
PaymentOperator op = PaymentOperator(operator);
op.authorize(paymentInfo, amount, tokenCollector, "");
op.capture(paymentInfo, amount);

Deployed Addresses

Source of truth: @x402r/sdk

Architecture

System Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                              USER INTERACTIONS                               │
├─────────────────────────────────────────────────────────────────────────────┤
│  Payer              Receiver              Designated Address (operator-config) │
│    │                   │                         │                           │
│    │  authorize()      │  capture()              │  capture() (if configured)│
│    │  freeze()         │  charge()               │  void()    (if configured)│
│    │  requestRefund()  │  requestRefund()        │  refund()  (if configured)│
└────┼───────────────────┼─────────────────────────┼───────────────────────────┘
     │                   │                         │
     ▼                   ▼                         ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          PAYMENT OPERATOR                                    │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Condition Slots (before action)    Hook Slots (after action)       │   │
│  │  ─────────────────────────────────  ─────────────────────────────   │   │
│  │  AUTHORIZE_PRE_ACTION_CONDITION ──────► AUTHORIZE_POST_ACTION_HOOK  │   │
│  │  CHARGE_PRE_ACTION_CONDITION ─────────► CHARGE_POST_ACTION_HOOK     │   │
│  │  CAPTURE_PRE_ACTION_CONDITION ────────► CAPTURE_POST_ACTION_HOOK    │   │
│  │  VOID_PRE_ACTION_CONDITION ───────────► VOID_POST_ACTION_HOOK       │   │
│  │  REFUND_PRE_ACTION_CONDITION ─────────► REFUND_POST_ACTION_HOOK     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    │                                        │
│  Owner Functions (7-day Timelock):  │                                        │
│  - queueFeesEnabled()              │                                        │
│  - executeFeesEnabled()            │                                        │
│  - cancelFeesEnabled()             │                                        │
└────────────────────────────────────┼────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         AUTH CAPTURE ESCROW                                  │
│                    (Base Commerce Payments)                                  │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Payment State Machine:                                              │   │
│  │                                                                      │   │
│  │  NonExistent ──authorize()──► InEscrow ──capture()──► Captured      │   │
│  │                                  │                        │          │   │
│  │                     void()       │       refund()         │          │   │
│  │                     reclaim()    │                        │          │   │
│  │                                  ▼                        ▼          │   │
│  │                            ┌─────────────────────────────────┐      │   │
│  │                            │           Settled               │      │   │
│  │                            └─────────────────────────────────┘      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

Condition Combinator Pattern

Conditions are composable plugins that control access to operator actions:

┌──────────────────────────────────────────────────────────────────┐
│                     CONDITION COMBINATORS                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  AndCondition([A, B, C])  ──►  A && B && C                       │
│  OrCondition([A, B])      ──►  A || B                            │
│  NotCondition(A)          ──►  !A                                │
│                                                                   │
│  Example: Capture requires (Receiver OR DesignatedAddr) AND EscrowPassed│
│                                                                   │
│  OrCondition([                                                   │
│    ReceiverCondition,                                            │
│    StaticAddressCondition(designatedAddr)                        │
│  ])                                                              │
│    └──► AndCondition([                                           │
│           <above>,                                               │
│           EscrowPeriod                                           │
│         ])                                                       │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Escrow Period & Freeze Flow

Freeze and EscrowPeriod are now separate, composable modules:

  • EscrowPeriod: ICondition that blocks capture during the escrow period
  • Freeze: Standalone ICondition with freeze()/unfreeze() methods

Compose them via AndCondition([escrowPeriod, freeze]) when you want both behaviors.

Timeline:
├─────────────── ESCROW_PERIOD (e.g., 7 days) ───────────────┼──── Post-Escrow ────►
│                                                             │
│  [Payer can freeze via Freeze contract]                    │  [Capture allowed]
│  [Capture blocked by EscrowPeriod]                         │  [Freeze blocked]
│                                                             │
│  Freeze.freeze() ──► PaymentFrozen                         │
│  Freeze.unfreeze() ──► PaymentUnfrozen                     │
│                                                             │
└─────────────────────────────────────────────────────────────┴─────────────────────►

MEV Protection: Payers should freeze EARLY, not at deadline.
                Use private mempool (Flashbots Protect) if freezing near expiry.

Contract Relationships

┌─────────────────────────┐
│ PaymentOperatorFactory  │◄─── Owner (Multisig in production)
└────────────┬────────────┘
             │ deploys
             ▼
┌─────────────────────────┐      ┌─────────────────────────┐
│   PaymentOperator       │─────►│    AuthCaptureEscrow    │
│  (per-config instance)  │      │   (shared singleton)    │
└────────────┬────────────┘      └─────────────────────────┘
             │ uses
             ▼
┌─────────────────────────────────────────────────────────────┐
│                    PLUGGABLE CONDITIONS                      │
├─────────────────────────────────────────────────────────────┤
│  Access Conditions:          │  Time/State Conditions:      │
│  - PayerCondition            │  - EscrowPeriod              │
│  - ReceiverCondition         │  - Freeze                    │
│  - StaticAddressCondition    │                              │
│  - AlwaysTrueCondition       │                              │
├─────────────────────────────────────────────────────────────┤
│  Combinators:                │  Hooks (Optional):       │
│  - AndCondition              │  - PaymentIndexRecorderHook      │
│  - OrCondition               │  - HookCombinator        │
│  - NotCondition              │                              │
├─────────────────────────────────────────────────────────────┤
│  Auxiliary:                  │                              │
│  - RefundRequest             │                              │
│  - FreezeFactory             │                              │
└─────────────────────────────────────────────────────────────┘

Roles & Permissions

Role Capabilities
Payer authorize(), freeze(), unfreeze(), requestRefund(), cancelRefundRequest(), void() (after expiry)
Receiver capture() (if condition allows), charge()
Designated Address Per operator configuration - can include void(), refund(), capture() (arbiter, service provider, DAO, etc.)
Owner queueFeesEnabled(), executeFeesEnabled(), cancelFeesEnabled(), rescueETH()

Authorization Expiry: The PaymentInfo struct includes an authorizationExpiry field from base commerce-payments. Payers can set this to limit how long receivers can charge funds. Set to type(uint48).max for no expiry, or specify a timestamp for time-limited authorizations (useful for subscriptions). After expiry, payers can reclaim unused funds via void().

Security Features

Feature Implementation
Reentrancy Protection ReentrancyGuardTransient on escrow
CEI Pattern All functions: Checks → Effects → Interactions
2-Step Ownership Solady's requestOwnershipHandover() + completeOwnershipHandover()
7-day Timelock Fee changes require queue → wait → execute
Multisig Requirement Owner must be Gnosis Safe in production
Incident Response See SECURITY.md

⛽ Gas Benchmarks

Typical gas costs for common operations (measured with via-IR optimization and reentrancy protection):

Core Operations

Operation Gas Cost With Indexing Notes
Payment Authorization ~231,000 ~273,000 Minimal storage (fees only)
Capture ~65,000 ~65,000 Capture after escrow period
Direct Charge ~285,000 ~327,000 Immediate capture (no escrow)
Void ~45,000 ~45,000 Cancel authorization, return funds to payer
Refund ~50,000 ~50,000 Return captured funds via ReceiverRefundCollector
Freeze Payment ~50,000 ~50,000 Payer freezes during escrow

Implementation: Payment indexing is optional via PaymentIndexRecorderHook. Deploy with indexing for on-chain queries (+42k gas first, +22k subsequent) or skip for gas savings when using external indexers (The Graph).

Condition Evaluation

Conditions Gas Cost Scaling
1 condition ~50,000 Single check
2 conditions (AND) ~75,000 Linear
5 conditions (AND) ~150,000 Linear
10 conditions (MAX) ~479,000 Near-linear

Recommended Complexity: Keep combinator depth ≤ 5 for optimal gas efficiency.

Token Rejection (Safety)

Test Gas Cost Result
Fee-on-transfer detection ~473,000 ✅ Rejects (strict balance check)
Rebasing token detection ~485,000 ✅ Detects accounting mismatch
Standard ERC20 ~473,000 ✅ Accepts

Token Safety: Protocol intentionally rejects fee-on-transfer and rebasing tokens to prevent accounting errors. See TOKENS.md for details.

Gas Optimization

Already Implemented:

  • ✅ Solady library (assembly-optimized)
  • ✅ Via-IR compilation
  • ✅ ReentrancyGuardTransient (transient storage, EIP-1153)
  • ✅ Immutable variables
  • ✅ Packed storage layout
  • ✅ Custom errors

Status: Gas costs are excellent for the security features provided. See GAS_OPTIMIZATION_REPORT.md for detailed analysis.

Network Cost Estimates

Estimated transaction costs on different networks (at typical gas prices):

Network Gas Price Authorization Capture Charge
Base Mainnet 0.001 gwei ~$0.0002 ~$0.0001 ~$0.0003
Base Sepolia Free Free Free Free
Ethereum L1 30 gwei ~$6.93 ~$1.95 ~$8.55

Recommendation: Deploy on Base for low-cost transactions (100-1000x cheaper than Ethereum L1).

Comparison with Alternatives

Protocol Authorization Capture Notes
x402r ~231k ~65k Minimal storage + reentrancy protection + flexible conditions
Gnosis Safe ~300k ~250k Multi-sig overhead, less flexible
Uniswap Permit2 ~150k ~100k Signature-based, no escrow
Superfluid ~400k Streaming Continuous flow, different model

Trade-off: Competitive gas costs with significantly better security and flexibility ✓

Pagination Queries (On-Chain)

Optional Feature: Deploy PaymentIndexRecorderHook to enable on-chain payment lookups.

Query Type Gas Cost Notes
Get 10 payments ~20,000 Paginated query (hash + amount)
Get 50 payments ~82,000 Scales linearly with count
Get single payment ~2,000 Direct index access

API: PaymentIndexRecorderHook.getPayerPayments(address, offset, count) returns (bytes32[] hashes, uint256 total):

  • hashes: Array of payment hashes for escrow lookup
  • total: Total number of payments for this address

Note: For timestamps, use EscrowPeriod which tracks authorization times. For amounts, query the escrow's paymentState(hash).capturableAmount.

  • With indexing: On-chain queries available, no external indexer needed
  • Without indexing: Use external indexer (The Graph) for lower gas costs
  • Fully on-chain, decentralized when enabled
  • Bounded gas cost (never unbounded array returns)

Gas Monitoring

Gas costs are continuously monitored in CI/CD:

  • Baseline: Updated on every merge to main
  • Regression Detection: PRs fail if gas increases > 5%
  • Nightly Benchmarks: Tracked in .gas-snapshot

See CI_CD_GUIDE.md for details.


Commerce Payments Contracts

The commerce-payments contracts provide refund functionality for Base Commerce Payments authorizations:

  • PaymentOperator: src/commerce-payments/operator/arbitration/PaymentOperator.sol

    • Generic operator contract with pluggable conditions for flexible authorization logic. Supports marketplace, subscriptions, streaming, DAO governance, and custom payment flows.
  • RefundRequest: src/commerce-payments/requests/refund/RefundRequest.sol

    • Contract for managing refund requests for Base Commerce Payments authorizations. Users can create refund requests, cancel their own pending requests, and merchants or arbiters can approve or deny them based on capture status.

Freeze Module

Freeze is a standalone ICondition contract with freeze()/unfreeze() methods. It's separate from EscrowPeriod for better composability.

Deploy via FreezeFactory:

// Deploy Freeze with freeze/unfreeze conditions and optional EscrowPeriod constraint
address freeze = freezeFactory.deploy(
    freezeCondition,      // ICondition - who can freeze (e.g., PayerCondition)
    unfreezeCondition,    // ICondition - who can unfreeze (e.g., PayerCondition)
    freezeDuration,       // uint256 - how long freeze lasts (0 = permanent until unfrozen)
    escrowPeriodContract  // address(0) = unconstrained, or EscrowPeriod address
);

Freeze/Unfreeze conditions determine who can freeze/unfreeze using ICondition contracts:

Condition Description
PayerCondition Allows the payment's payer
ReceiverCondition Allows the payment's receiver
StaticAddressCondition(addr) Allows a designated address (arbiter, service provider, DAO, platform, etc.)
AlwaysTrueCondition Allows anyone

Example:

// 1. Deploy EscrowPeriod (7 days, operator-only recording)
address escrowPeriod = escrowPeriodFactory.deploy(7 days, bytes32(0));

// 2. Deploy Freeze (payer can freeze/unfreeze, 3-day duration, constrained to escrow period)
address freeze = freezeFactory.deploy(payerCondition, payerCondition, 3 days, escrowPeriod);

// 3. Compose for capture condition: must pass both escrow period AND not be frozen
address capturePreActionCondition = address(new AndCondition([ICondition(escrowPeriod), ICondition(freeze)]));

Composition Patterns:

  • Escrow period only: capturePreActionCondition = escrowPeriod
  • Freeze only: capturePreActionCondition = freeze
  • Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze])

PaymentOperatorFactory API

The PaymentOperatorFactory provides a single generic deployOperator(OperatorConfig) method. There are no convenience methods - users must construct the full OperatorConfig struct:

struct OperatorConfig {
    address feeReceiver;
    address feeCalculator;
    address authorizePreActionCondition;
    address authorizePostActionHook;
    address chargePreActionCondition;
    address chargePostActionHook;
    address capturePreActionCondition;
    address capturePostActionHook;
    address voidPreActionCondition;
    address voidPostActionHook;
    address refundPreActionCondition;
    address refundPostActionHook;
}

Example: Deploy a marketplace operator with arbiter

// Deploy arbiter condition via factory (deterministic address, idempotent)
address arbiterCondition = staticAddressConditionFactory.deploy(arbiterAddress);

PaymentOperatorFactory.OperatorConfig memory config = PaymentOperatorFactory.OperatorConfig({
    feeReceiver: arbiterAddress,                                  // Arbiter earns fees for dispute resolution
    feeCalculator: address(feeCalc),                              // Operator fee calculator
    authorizePreActionCondition: address(0),                      // Anyone can authorize
    authorizePostActionHook: address(0),                          // No post-action work
    chargePreActionCondition: address(receiverCondition),         // Only receiver can charge
    chargePostActionHook: address(0),
    capturePreActionCondition: address(escrowPeriodCondition),    // Capture allowed only after escrow period
    capturePostActionHook: address(escrowPeriodHook),             // Record timestamp
    voidPreActionCondition: arbiterCondition,                     // Only arbiter can void
    voidPostActionHook: address(0),
    refundPreActionCondition: arbiterCondition,                   // Only arbiter for post-capture refunds
    refundPostActionHook: address(0)
});
address operator = factory.deployOperator(config);

Note: address(0) for a condition means "allow all" (no restriction). address(0) for a hook means "no-op" (no post-action work).

Optional Payment Indexing

PaymentIndexRecorderHook provides on-chain payment lookups by payer/receiver. Deploy once and share across operators:

// Deploy indexer (optional)
PaymentIndexRecorderHook indexHook = new PaymentIndexRecorderHook(address(escrow), bytes32(0));

// Option 1: Enable indexing
PaymentOperatorFactory.OperatorConfig memory config = PaymentOperatorFactory.OperatorConfig({
    // ...
    authorizePostActionHook: address(indexHook),  // Index on authorize
    chargePostActionHook: address(indexHook),     // Index on charge
    // ...
});

// Option 2: Skip indexing (lower gas, use The Graph instead)
PaymentOperatorFactory.OperatorConfig memory config = PaymentOperatorFactory.OperatorConfig({
    // ...
    authorizePostActionHook: address(0),  // No indexing
    chargePostActionHook: address(0),     // No indexing
    // ...
});

// Query payments (requires indexing enabled)
(bytes32[] memory hashes, uint256 total) = indexHook.getPayerPayments(payer, 0, 10);
// hashes[0] - Payment hash for escrow lookup
// For amounts: escrow.paymentState(hashes[0]).capturableAmount
// For timestamps: use EscrowPeriod.authorizationTimes(hash)

Benefits:

  • Efficient Storage: Stores only payment hashes (minimal gas cost)
  • Gas Savings: ~55k per authorization when indexing disabled
  • Flexibility: Deploy with or without on-chain queries
  • Composability: Combine with other hooks via HookCombinator
  • No Duplication: Use EscrowPeriod for timestamps, escrow for amounts

When to use indexing:

  • ✅ Need on-chain payment history queries
  • ✅ Building fully decentralized applications
  • ✅ Want to avoid external dependencies

When to skip indexing:

  • ✅ Using external indexer (The Graph, Dune)
  • ✅ Optimizing for minimum gas costs
  • ✅ Don't need on-chain payment history

Factory Deployment

All deployment scripts use factory contracts that provide:

  • Deterministic addresses (CREATE2): Same inputs = same address, even if not yet deployed
  • Idempotent deployment: Safe to call multiple times, returns existing if already deployed
  • Shared configuration: Escrow, protocol fees set once in factory
  • Centralized owner control: Factory owner controls all deployed instances

Foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.

Foundry consists of:

  • Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
  • Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
  • Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
  • Chisel: Fast, utilitarian, and verbose solidity REPL.

Documentation

https://book.getfoundry.sh/

Setup

Environment Variables

  1. Copy the example environment file:

    cp .env.example .env
  2. Edit .env and add your API keys:

    ETHERSCAN_API_KEY=your_basescan_api_key_here
    PRIVATE_KEY=your_private_key_here

    Get your Basescan API key from: https://basescan.org/myapikey

  3. Load environment variables before running commands:

    source .env

    Or export them manually:

    export ETHERSCAN_API_KEY=your_api_key
    export PRIVATE_KEY=your_private_key

Usage

Build

$ forge build

Test

$ forge test

Format

$ forge fmt

Gas Snapshots

$ forge snapshot

Anvil

$ anvil

Deploy

Deploy contracts using the deployment scripts. The --verify flag will automatically verify contracts on Basescan using the ETHERSCAN_API_KEY from your .env file.

Cast

$ cast <subcommand>

Help

$ forge --help
$ anvil --help
$ cast --help

About

smart contracts for x402r in solidity

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages