Trustless escrow inside Towns. Lock USDC on Base → Release/Refund → Receipt.
On-chain escrow contracts for peer-to-peer deals with optional arbitration. Built for the Towns Bot Competition.
- Buyer locks USDC in a smart contract
- Seller sees funds are guaranteed
- Release when satisfied or Refund after deadline
- Optional Arbiter for dispute resolution
- All on Base L2 for low gas fees
EscrowFactory.sol- Creates and tracks all escrow dealsEscrow.sol- Individual escrow logic (one per deal)
CREATED → FUNDED → RELEASED/REFUNDED
↓
DISPUTED → RESOLVED (if arbiter exists)
✅ NonReentrant guards
✅ SafeERC20 token handling
✅ Role-based access control
✅ Deadline-based refunds
✅ Optional dispute resolution
✅ USDC-only whitelist (extensible)
# Clone repo
git clone <your-repo>
cd handshake-escrow
# Install dependencies
forge install foundry-rs/forge-std --no-commit
forge install OpenZeppelin/openzeppelin-contracts --no-commit
# Copy environment variables
cp .env.example .env
# Edit .env with your private key and RPC URLs# Run all tests
forge test
# Run with verbosity
forge test -vvv
# Run specific test
forge test --match-test test_Release_Success
# Gas report
forge test --gas-reportExpected output: All tests passing ✅
# Load environment
source .env
# Deploy
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--broadcast \
--verify \
-vvvv
# Or use the testnet-specific function
forge script script/Deploy.s.sol:DeployScript \
--sig "runTestnet()" \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--broadcast \
--verify \
-vvvvforge script script/Deploy.s.sol:DeployScript \
--rpc-url $BASE_RPC_URL \
--broadcast \
--verify \
-vvvv- EscrowFactory:
TBD - USDC:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
- EscrowFactory:
TBD - USDC:
0x036CbD53842c5426634e7929541eC2318f3dCF7e
address seller = 0x...;
address usdc = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
uint256 amount = 100e6; // 100 USDC
uint256 deadline = block.timestamp + 48 hours;
address arbiter = 0x...; // or address(0) for none
bytes32 memoHash = keccak256("Logo design for project");
address escrowAddr = factory.createEscrow(
seller,
usdc,
amount,
deadline,
arbiter,
memoHash
);IERC20(usdc).approve(escrowAddr, amount);
Escrow(escrowAddr).fund();Escrow(escrowAddr).release(); // Pays seller// After deadline passes
Escrow(escrowAddr).refundAfterDeadline(); // Returns to buyer// Buyer or Seller opens dispute
Escrow(escrowAddr).openDispute();
// Arbiter resolves
Escrow(escrowAddr).resolve(true); // true = pay seller, false = refund buyer- ✅ Factory: Create escrow, token whitelist
- ✅ Fund: Success, unauthorized, after deadline
- ✅ Release: Success, unauthorized, wrong status
- ✅ Refund: Success, before deadline, wrong status
- ✅ Dispute: Open, resolve, no arbiter, unauthorized
- ✅ Registry: Track buyer/seller escrows
Coverage: 100% of functions
- No custody: Funds never held by bot/backend
- Immutable roles: Buyer/seller/arbiter set at creation
- Deadline enforcement: Refund only after deadline
- Reentrancy protection: NonReentrant on all state-changing functions
- Token whitelist: Only approved tokens (USDC)
| Function | Gas Cost |
|---|---|
| createEscrow | ~250k |
| fund | ~90k |
| release | ~55k |
| refundAfterDeadline | ~55k |
| openDispute | ~45k |
| resolve | ~55k |
Estimates on Base Sepolia. Mainnet may vary.
# Format code
forge fmt
# Check formatting
forge fmt --check
# Update dependencies
forge update
# Build
forge build
# Clean
forge clean- OpenZeppelin Contracts - SafeERC20, ReentrancyGuard
- Foundry - Testing & deployment
- Easy indexing of all escrows
- Track user activity (buyer/seller history)
- Emit events for bot indexer
- Gas savings
- Cannot be manipulated post-creation
- Clear audit trail
- Simplifies MVP
- Most liquid stablecoin on Base
- Easy to extend later
- ✅ Deploy to Base mainnet
- 🔄 Integrate with Towns bot
- 🔄 Build miniapp frontend
- 📈 Add analytics dashboard
- 🌟 Launch & gather feedback
MIT
Built for Towns Bot Competition. Feedback welcome!
Built with 🤝 for trustless peer-to-peer deals