A production-grade Solidity smart contract system for group payments with atomic settlement, built with Foundry and OpenZeppelin.
GroupPay enables N-of-N group payments where all participants must contribute before funds are released to the merchant. Key features:
- Atomic Settlement: All-or-nothing payment processing
- Deadline-based: Automatic refund capability after deadline
- Cashback Support: Optional cashback via global treasury
- Factory Pattern: EIP-1167 minimal proxy clones for gas efficiency
- Production Security: ReentrancyGuard, SafeERC20, comprehensive error handling
GroupFactory (EIP-1167 Factory)
├── GroupEscrow (Implementation + Clones)
│ ├── State Machine: Pending → Settled/Refunding → Closed
│ ├── Participant Management (allowlist)
│ └── Atomic Settlement Logic
├── CashbackTreasury (Optional)
│ ├── Authorized Escrow Management
│ └── Equal Distribution Payouts
└── MockUSDC (Testing)
└── 6-decimal USDC simulation
✅ No Partial Settlement: settle() only succeeds if paidCount == N and block.timestamp <= deadline
✅ Single Deposit: Each participant deposits exactly perUser amount once
✅ Refund Correctness: Participants can never withdraw more than deposited
✅ Conservation: Σ deposits = merchantPayout + Σ refunds + Σ cashback
✅ Safety: CEI pattern, nonReentrant, SafeERC20 transfers
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Install dependencies
forge installPerfect for development and testing. Uses fake USDC and pre-funded accounts.
# 1. Start local blockchain
anvil
# 2. Deploy contracts (new terminal)
forge script script/DeployLocal.s.sol --rpc-url http://localhost:8545 --broadcast
# 3. Run comprehensive tests
forge test --gas-report
# 4. Create test group
forge script script/CreateGroup.s.sol --rpc-url http://localhost:8545 --broadcast✅ Local deployment gives you:
- All contracts deployed instantly
- 5 pre-funded test accounts (1,000 USDC each)
- Treasury funded with 10,000 USDC
- Real gas cost estimates
- Complete working demo
Deploy to real blockchain for team demos and integration testing.
# 1. Get Base Sepolia ETH (free)
# Visit: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet
# Need: ~0.01 ETH for deployment
# 2. Get BaseScan API key (free)
# Visit: https://basescan.org/apis
# Create account → Get API key# Copy template
cp .env.example .env
# Edit .env file with YOUR details:
PRIVATE_KEY=0x1234567890abcdef... # Your wallet private key (with 0x prefix!)
BASE_SEPOLIA_RPC=https://sepolia.base.org
BASESCAN_API_KEY=ABC123... # Your BaseScan API key# Deploy all contracts to Base Sepolia
source .env && forge script script/DeployBaseSepolia.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast
# ✅ IMPORTANT: Copy the contract addresses from output!
# Add them to your .env file:
# FACTORY_ADDRESS=0x...
# TREASURY_ADDRESS=0x...
# USDC_ADDRESS=0x...# Create a test group on live testnet
source .env && forge script script/CreateGroup.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast
# ✅ Your group is now live at the escrow address shown!
# View on BaseScan: https://sepolia.basescan.org/address/0x...| Operation | Gas Used | Cost (ETH) | Cost (USD)* |
|---|---|---|---|
| Deploy System | 1,967,913 | 0.000002 ETH | $0.008 |
| Create Group | 484,254 | 0.0000005 ETH | $0.002 |
| User Deposit | ~60,000 | 0.00000006 ETH | $0.0002 |
| Settlement | ~80,000 | 0.00000008 ETH | $0.0003 |
*Based on ETH = $4000, Base gas = 0.001 gwei
💡 Total cost per group payment: ~$0.0005 per user on Base
forge test -vv# Core escrow functionality
forge test --match-contract GroupEscrowTest -vv
# Cashback treasury
forge test --match-contract CashbackTest -vv
# Invariant/fuzz testing
forge test --match-contract InvariantsTest -vv
# Gas reporting
forge test --gas-reportfunction createGroup(CreateParams calldata params) external returns (address escrow);
struct CreateParams {
address merchant; // Receives funds
IERC20 token; // Payment token (USDC)
uint256 totalAmount; // Total to collect
uint256 perUser; // Amount per participant
uint256 deadline; // Deadline timestamp
address[] participants; // Allowlisted addresses
uint256 cashbackBps; // Cashback (0-10000 bps)
address treasury; // Treasury contract
}// Participant actions
function deposit() external;
function refund() external;
// Settlement
function settle() external;
function enableRefund() external;
// Views
function state() external view returns (State);
function timeRemaining() external view returns (uint256);
function myRequiredAmount() external view returns (uint256);// Owner functions
function fund(uint256 amount) external;
function withdraw(uint256 amount) external;
function authorizeEscrow(address escrow, bool authorized) external;
// Escrow function
function payoutEqual(IERC20 token, address[] calldata payees, uint256 perUserAmount) external;Pending ──┐
├─→ Settled ──→ Closed
└─→ Refunding ──→ Closed
Pending: Collecting deposits
Settled: All paid, funds to merchant + cashback
Refunding: Past deadline, partial payment
Closed: Final state
| Error | Condition |
|---|---|
NotParticipant() |
Caller not in allowlist |
AlreadyPaid() |
Participant deposited twice |
WrongAmount() |
Deposit ≠ perUser |
PastDeadline() |
Action after deadline |
NotAllPaid() |
Settlement with partial payment |
NotRefunding() |
Refund action in wrong state |
CashbackUnavailable() |
Treasury insufficient funds |
- EIP-1167 Clones: ~90% deployment gas savings vs new contracts
- Unchecked Arithmetic: Safe overflow assumptions where applicable
- Packed Storage: Efficient state variable layout
- Bitmap Option: Consider
mapping(uint256 => uint256)for large participant sets
Suggested improvements for scale:
// For >20 participants, consider bitmap tracking
mapping(uint256 => uint256) private _paidBitmap;
function _setPaid(address participant, bool paid) private {
uint256 index = _participantIndex[participant];
uint256 wordIndex = index / 256;
uint256 bitIndex = index % 256;
if (paid) {
_paidBitmap[wordIndex] |= (1 << bitIndex);
} else {
_paidBitmap[wordIndex] &= ~(1 << bitIndex);
}
}- Reentrancy:
nonReentranton all state-changing functions - Integer Overflow: Solidity 0.8+ built-in protection +
uncheckedwhere safe - Access Control: Participant allowlists, treasury authorization
- Token Safety: SafeERC20 for all transfers
- State Validation: Comprehensive state machine checks
- Deadline Enforcement: Strict timestamp validation
- Permit Support:
depositWithPermit()marked as TODO pending Base USDC permit verification - Participant Limit: Suggested max 20 participants to avoid gas limit issues
- Single Token: Currently supports one token type per escrow
- Immutable Params: Group parameters cannot be modified after creation
MIT
- Fork the repository
- Create feature branch (
git checkout -b feature/amazing-feature) - Add comprehensive tests
- Ensure all tests pass (
forge test) - Update documentation
- Submit pull request