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:
- USDC: View on BaseScan Sepolia (opens in a new tab)
- Multicall: TBD
Base Mainnet (Production)
USDC Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Multicall Contract: TBD (deployment pending)Explorer:
- USDC: View on BaseScan (opens in a new tab)
- Multicall: TBD
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:
| Parameter | Type | Description |
|---|---|---|
froms | address[] | Array of sender addresses (buyers) |
tos | address[] | Array of recipient addresses (providers) |
values | uint256[] | Array of USDC amounts (6 decimals) |
validAfters | uint256[] | Array of valid-after timestamps |
validBefores | uint256[] | Array of valid-before timestamps (expiry) |
nonces | bytes32[] | Array of unique nonces (prevents replay) |
vs | uint8[] | Array of signature v components (27 or 28) |
rs | bytes32[] | Array of signature r components |
ss | bytes32[] | 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 batchpaymentCount: Number of payments in the batchtotalAmount: 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 tofrom: Buyer addressto: Provider addressvalue: 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
| Operation | Gas Cost | USD Cost (Base) |
|---|---|---|
| Contract Deployment | ~500,000 gas | ~$0.05 (one-time) |
Per-Transaction Costs
| Operation | Gas Cost | USD 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/0xYourTxHashWhat to look for:
- Contract Interaction: Should show
USDCBatchSettlement - Method:
batchTransferWithAuth - Events: Look for
BatchSettledand multiplePaymentSettledevents - 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:
- Invalid signature: EIP-3009 signature malformed
- Nonce reused: Payment already settled
- Insufficient USDC: Buyer wallet has 0 USDC
- Authorization expired: Current time > validBefore
- 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
- Batch Settlement Overview - Economics and examples
- Settlement Triggers - When batches settle
- Settlement FAQ - Common questions
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:
- Developer Docs: docs.preimage-research.ai (opens in a new tab)
- Discord: Join our community (opens in a new tab)
- Email: developers@preimage-research.ai