Batch Settlement Contract

Batch Settlement Smart Contract

Technical reference for the USDCBatchSettlement smart contract that enables efficient micropayment settlement on blockchain networks.


Contract Overview

Purpose: Execute multiple EIP-3009 transferWithAuthorization calls in a single blockchain transaction.

Key Features:

  • ⚡ Processes 100+ payments in one transaction
  • 💰 50-100x gas cost reduction
  • 🔒 Non-custodial (contract doesn't hold funds)
  • 🔓 Permissionless (anyone can call - signatures prove authorization)
  • ⚙️ Atomic execution (all succeed or all revert)

Contract Addresses

Base Sepolia (Testnet)

USDC Contract: 0x036CbD53842c5426634e7929541eC2318f3dCF7e
Multicall Contract: TBD (deployment pending)

Explorer:

Base Mainnet (Production)

USDC Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Multicall Contract: TBD (deployment pending)

Explorer:

💡

Contract addresses will be updated here once deployed to production networks.


Contract Interface

batchTransferWithAuth

Execute multiple EIP-3009 authorizations in a single transaction.

function batchTransferWithAuth(
    address[] calldata froms,
    address[] calldata tos,
    uint256[] calldata values,
    uint256[] calldata validAfters,
    uint256[] calldata validBefores,
    bytes32[] calldata nonces,
    uint8[] calldata vs,
    bytes32[] calldata rs,
    bytes32[] calldata ss
) external returns (uint256 batchId);

Parameters:

ParameterTypeDescription
fromsaddress[]Array of sender addresses (buyers)
tosaddress[]Array of recipient addresses (providers)
valuesuint256[]Array of USDC amounts (6 decimals)
validAftersuint256[]Array of valid-after timestamps
validBeforesuint256[]Array of valid-before timestamps (expiry)
noncesbytes32[]Array of unique nonces (prevents replay)
vsuint8[]Array of signature v components (27 or 28)
rsbytes32[]Array of signature r components
ssbytes32[]Array of signature s components

Returns:

  • batchId (uint256): Unique identifier for this batch

Requirements:

  • All arrays must have the same length
  • Length must be > 0
  • Each EIP-3009 signature must be valid
  • Nonces must not have been used previously

Reverts if:

  • Array length mismatch
  • Empty batch
  • Invalid signature
  • Nonce already used
  • Insufficient USDC balance
  • Authorization expired

Events

BatchSettled

Emitted when a batch is successfully settled.

event BatchSettled(
    uint256 indexed batchId,
    uint256 paymentCount,
    uint256 totalAmount,
    address indexed submitter
);

Parameters:

  • batchId: Unique identifier for the batch
  • paymentCount: Number of payments in the batch
  • totalAmount: Total USDC transferred (sum of all values)
  • submitter: Address that submitted the batch transaction

Usage:

// Listen for batch settlements
contract.on("BatchSettled", (batchId, paymentCount, totalAmount, submitter) => {
  console.log(`Batch ${batchId}: ${paymentCount} payments, ${totalAmount} USDC`);
});

PaymentSettled

Emitted for each individual payment in a batch.

event PaymentSettled(
    uint256 indexed batchId,
    address indexed from,
    address indexed to,
    uint256 value
);

Parameters:

  • batchId: Batch this payment belongs to
  • from: Buyer address
  • to: Provider address
  • value: USDC amount transferred

Usage:

// Listen for individual payments
contract.on("PaymentSettled", (batchId, from, to, value) => {
  console.log(`Payment in batch ${batchId}: ${from}${to}: ${value} USDC`);
});

Full Contract Source

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
 
/**
 * @title USDCBatchSettlement
 * @notice Batches multiple EIP-3009 transferWithAuthorization calls into one transaction
 * @dev Optimizes gas costs for micropayment settlements
 * 
 * Key Features:
 * - Processes N signed authorizations in one transaction
 * - Gas cost: ~5000 gas per transfer (~$0.0001 on Base)
 * - No custody: contract doesn't hold funds
 * - Permissionless: anyone can call (signatures prove authorization)
 * 
 * Security:
 * - Validates each EIP-3009 signature
 * - Cannot modify amounts or recipients
 * - Nonce prevents replay attacks
 * - Atomic: all succeed or all revert
 */
interface IUSDC {
    function transferWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;
}
 
contract USDCBatchSettlement {
    /// @notice USDC contract address
    IUSDC public immutable usdc;
    
    /// @notice Emitted when a batch is successfully settled
    event BatchSettled(
        uint256 indexed batchId,
        uint256 paymentCount,
        uint256 totalAmount,
        address indexed submitter
    );
    
    /// @notice Emitted for each individual payment in a batch
    event PaymentSettled(
        uint256 indexed batchId,
        address indexed from,
        address indexed to,
        uint256 value
    );
    
    /// @notice Counter for batch IDs
    uint256 public batchCounter;
    
    /**
     * @notice Constructor
     * @param _usdcAddress USDC contract address for the network
     */
    constructor(address _usdcAddress) {
        require(_usdcAddress != address(0), "Invalid USDC address");
        usdc = IUSDC(_usdcAddress);
    }
    
    /**
     * @notice Batch process multiple EIP-3009 authorizations
     * @dev Anyone can call this function - signatures prove authorization
     * @param froms Array of sender addresses
     * @param tos Array of recipient addresses
     * @param values Array of amounts (6 decimals for USDC)
     * @param validAfters Array of valid after timestamps
     * @param validBefores Array of valid before timestamps
     * @param nonces Array of nonces (prevents replay)
     * @param vs Array of signature v components
     * @param rs Array of signature r components
     * @param ss Array of signature s components
     * @return batchId The ID of this batch
     */
    function batchTransferWithAuth(
        address[] calldata froms,
        address[] calldata tos,
        uint256[] calldata values,
        uint256[] calldata validAfters,
        uint256[] calldata validBefores,
        bytes32[] calldata nonces,
        uint8[] calldata vs,
        bytes32[] calldata rs,
        bytes32[] calldata ss
    ) external returns (uint256 batchId) {
        // Validate array lengths
        uint256 length = froms.length;
        require(length > 0, "Empty batch");
        require(length == tos.length, "Length mismatch: tos");
        require(length == values.length, "Length mismatch: values");
        require(length == validAfters.length, "Length mismatch: validAfters");
        require(length == validBefores.length, "Length mismatch: validBefores");
        require(length == nonces.length, "Length mismatch: nonces");
        require(length == vs.length, "Length mismatch: vs");
        require(length == rs.length, "Length mismatch: rs");
        require(length == ss.length, "Length mismatch: ss");
        
        // Increment batch counter
        batchId = ++batchCounter;
        
        // Calculate total amount for event
        uint256 totalAmount = 0;
        for (uint256 i = 0; i < length; i++) {
            totalAmount += values[i];
        }
        
        // Process each authorization
        for (uint256 i = 0; i < length; i++) {
            // Call USDC's transferWithAuthorization
            // This validates the signature and executes the transfer
            usdc.transferWithAuthorization(
                froms[i],
                tos[i],
                values[i],
                validAfters[i],
                validBefores[i],
                nonces[i],
                vs[i],
                rs[i],
                ss[i]
            );
            
            // Emit event for individual payment
            emit PaymentSettled(batchId, froms[i], tos[i], values[i]);
        }
        
        // Emit batch settlement event
        emit BatchSettled(batchId, length, totalAmount, msg.sender);
        
        return batchId;
    }
    
    /**
     * @notice Get contract version
     * @return version string
     */
    function version() external pure returns (string memory) {
        return "1.0.0";
    }
}

Gas Analysis

Deployment Costs

OperationGas CostUSD Cost (Base)
Contract Deployment~500,000 gas~$0.05 (one-time)

Per-Transaction Costs

OperationGas CostUSD Cost (Base)
Batch Header~50,000 gas~$0.005
Per Transfer~5,000 gas~$0.0005
100 Transfers~550,000 gas~$0.055
Per Payment (avg)~5,500 gas~$0.00055

Key Insight: Gas per payment decreases as batch size increases. With 100 payments, gas overhead is ~1% for $0.01 payments.


Usage Examples

Example 1: Submit Batch (JavaScript)

import { ethers } from 'ethers';
 
// Contract ABI
const abi = [...]; // USDCBatchSettlement ABI
 
// Connect to contract
const provider = new ethers.providers.JsonRpcProvider('https://mainnet.base.org');
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, wallet);
 
// Prepare batch data
const batch = {
  froms: ['0xBuyer1', '0xBuyer2', ...],
  tos: ['0xProvider1', '0xProvider2', ...],
  values: [1000000, 2000000, ...], // 6 decimals
  validAfters: [0, 0, ...],
  validBefores: [1735689600, 1735689600, ...],
  nonces: ['0xnonce1', '0xnonce2', ...],
  vs: [27, 28, ...],
  rs: ['0xr1', '0xr2', ...],
  ss: ['0xs1', '0xs2', ...]
};
 
// Submit batch
const tx = await contract.batchTransferWithAuth(
  batch.froms,
  batch.tos,
  batch.values,
  batch.validAfters,
  batch.validBefores,
  batch.nonces,
  batch.vs,
  batch.rs,
  batch.ss
);
 
console.log(`Transaction submitted: ${tx.hash}`);
 
// Wait for confirmation
const receipt = await tx.wait();
console.log(`Batch settled in block ${receipt.blockNumber}`);
 
// Get batch ID from event
const event = receipt.events.find(e => e.event === 'BatchSettled');
console.log(`Batch ID: ${event.args.batchId}`);
console.log(`Payments: ${event.args.paymentCount}`);
console.log(`Total: ${event.args.totalAmount} USDC`);

Example 2: Listen for Settlements

// Listen for all batch settlements
contract.on("BatchSettled", (batchId, paymentCount, totalAmount, submitter) => {
  console.log(`
    Batch ID: ${batchId}
    Payments: ${paymentCount}
    Total USDC: ${ethers.utils.formatUnits(totalAmount, 6)}
    Submitter: ${submitter}
  `);
});
 
// Listen for specific wallet's payments
contract.on("PaymentSettled", (batchId, from, to, value, event) => {
  if (to === MY_WALLET_ADDRESS) {
    console.log(`
      Received payment in batch ${batchId}
      From: ${from}
      Amount: ${ethers.utils.formatUnits(value, 6)} USDC
    `);
  }
});

Example 3: Verify Settlement on Blockchain

// Query past events
const filter = contract.filters.BatchSettled(null, null, null, SUBMITTER_ADDRESS);
const events = await contract.queryFilter(filter, -1000, 'latest');
 
events.forEach(event => {
  console.log(`
    Block: ${event.blockNumber}
    Batch ID: ${event.args.batchId}
    Payments: ${event.args.paymentCount}
    Total: ${ethers.utils.formatUnits(event.args.totalAmount, 6)} USDC
    TX: ${event.transactionHash}
  `);
});

Verification

On BaseScan

View any batch settlement transaction:

https://basescan.org/tx/0xYourTxHash

What to look for:

  1. Contract Interaction: Should show USDCBatchSettlement
  2. Method: batchTransferWithAuth
  3. Events: Look for BatchSettled and multiple PaymentSettled events
  4. USDC Transfers: Internal transactions showing USDC movements

Via RPC

// Get transaction receipt
const receipt = await provider.getTransactionReceipt(txHash);
 
// Parse events
const iface = new ethers.utils.Interface(abi);
receipt.logs.forEach(log => {
  try {
    const parsed = iface.parseLog(log);
    if (parsed.name === 'BatchSettled') {
      console.log(`Batch ID: ${parsed.args.batchId}`);
      console.log(`Payments: ${parsed.args.paymentCount}`);
      console.log(`Total: ${ethers.utils.formatUnits(parsed.args.totalAmount, 6)} USDC`);
    }
  } catch (e) {
    // Not our event
  }
});

Security Considerations

Permissionless Design

Anyone can call batchTransferWithAuth - this is intentional and secure because:

  • ✅ Each payment requires a valid EIP-3009 signature from the buyer
  • ✅ Signatures cannot be forged without the buyer's private key
  • ✅ Nonces prevent replay attacks
  • ✅ Contract validates all parameters and signatures
  • ✅ Amounts and recipients are locked in the signature

No privileged access is required to submit batches.

Non-Custodial

The contract never holds funds:

  • Payments transfer directly from buyer to provider
  • Contract acts as a dispatcher, not a custodian
  • No approval or allowance required

Atomic Execution

All payments in a batch succeed or all revert:

  • If one payment fails, entire batch reverts
  • Prevents partial execution
  • Ensures consistent state

Nonce Tracking

USDC contract tracks nonces to prevent replay:

  • Each nonce can only be used once
  • Attempting to reuse a nonce reverts
  • Nonce management handled by USDC contract

Troubleshooting

Batch Submission Fails

Error: "execution reverted"

Possible causes:

  1. Invalid signature: EIP-3009 signature malformed
  2. Nonce reused: Payment already settled
  3. Insufficient USDC: Buyer wallet has 0 USDC
  4. Authorization expired: Current time > validBefore
  5. Array length mismatch: Arrays have different lengths

Debug steps:

// Test each signature individually
for (let i = 0; i < batch.froms.length; i++) {
  try {
    await usdc.transferWithAuthorization(
      batch.froms[i],
      batch.tos[i],
      batch.values[i],
      batch.validAfters[i],
      batch.validBefores[i],
      batch.nonces[i],
      batch.vs[i],
      batch.rs[i],
      batch.ss[i]
    );
    console.log(`Payment ${i}: OK`);
  } catch (e) {
    console.error(`Payment ${i}: FAILED - ${e.message}`);
  }
}

Event Not Appearing

Issue: Transaction succeeds but events not showing

Solution:

  • Wait for block confirmation (12 seconds on Base)
  • Check if transaction was actually successful
  • Verify you're connected to correct network
  • Try querying events by block range instead of real-time

Related Documentation


Contract Audit Status

⚠️

Audit Status: Contract audit in progress. Do not use in production with large amounts until audit is complete.

Current Status: Deployed to Base Sepolia testnet for testing
Expected Audit Completion: Q1 2025
Auditor: TBD


Support

For technical questions about the smart contract: