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