A complete decentralized application demonstrating gas optimization through transaction batching, EIP-712 meta-transactions, and sponsorship pool mechanisms on the Sepolia testnet.
Built for Web3Assam — Driving blockchain education and adoption across Northeast India.
- Problem Statement
- Solution Overview
- System Architecture
- Smart Contracts
- Meta-Transaction Support
- Gas Sponsorship Logic
- Frontend Interface
- Gas Savings Analysis
- Security Model
- Setup & Deployment
- Testing & Validation
- Assumptions & Limitations
- File Structure
- References
Ethereum transactions require users to pay gas fees for every on-chain action. In applications with frequent or multi-step interactions, this creates:
- High costs — Each transaction carries a 21,000 gas base overhead
- Poor UX — Users must approve multiple MetaMask popups per workflow
- Onboarding friction — New users must acquire native ETH before using any dApp
This project addresses these issues by building a Gas Fee Optimizer and Batch Transaction System that combines:
- Transaction Batching — Amortize the 21,000 base gas cost across N operations
- Meta-Transactions — Users sign off-chain; relayers execute on-chain
- Gas Sponsorship — Optional full/partial subsidization of gas fees
+----------------+ EIP-712 Sign +----------------+ Single TX +-------------------+
| User Wallet |---(no gas)---->| Relayer |---(batched)-->| BatchExecutor |
| (MetaMask) | | Server | | (Smart Contract) |
+----------------+ +-------+--------+ +---------+---------+
| Reimburse | Execute
v v
+----------------+ +-------------------+
| GasSponsor | | SampleToken |
| (Pool) | | (ERC-20) |
+----------------+ +-------------------+
User pays: 0 gas. The relayer handles execution, optionally reimbursed by the sponsor pool.
Full architecture details:
ARCHITECTURE.md
| Component | Type | Role |
|---|---|---|
| BatchExecutor | Solidity Contract | Verifies EIP-712 signatures, tracks nonces, enforces deadlines, executes batched calls |
| GasSponsor | Solidity Contract | Manages sponsorship pool with 6-layer constraint system |
| SampleToken | Solidity Contract | Meta-tx-aware ERC-20 (trusted forwarder pattern) |
| Relayer | Node.js Server | Queues signed requests, auto-flushes batches on timer |
| Frontend | HTML/JS | Wallet connection, action builder, EIP-712 signing UI |
- Trusted Forwarder (ERC-2771) — BatchExecutor appends real sender to calldata
- EIP-712 Structured Data — Human-readable, domain-bound signatures
- Nonce-Based Replay Protection — Sequential per-user nonces
- Queue-and-Flush — Time/size-triggered batch submission
- Defense-in-Depth Sponsorship — 6 independent constraint layers
- User connects wallet (MetaMask on Sepolia)
- User builds N token transfer actions in the UI
- User signs each ForwardRequest via EIP-712 (no gas)
- Relayer receives signed requests via
POST /api/relay - Relayer batches requests and calls
executeBatch()(one TX) - BatchExecutor verifies each signature, checks nonces, executes calls
- GasSponsor reimburses relayer (if funded & within limits)
BatchExecutor.sol (source)
The core contract implementing batched meta-transaction execution.
Key Features:
- EIP-712 domain separator (chain + contract bound)
verify(request, signature)— On-chain signature verificationexecuteBatch(requests[], signatures[])— Single-TX batch execution- Sequential nonce tracking per user
- Gas-isolated sub-calls with configurable limits
- Sender identity propagation to target contracts
ForwardRequest Struct:
struct ForwardRequest {
address from; // Original sender (user)
address to; // Target contract
uint256 value; // ETH to send (usually 0)
uint256 gas; // Gas limit for this sub-call
uint256 nonce; // User's sequential nonce (replay protection)
uint256 deadline; // Block timestamp after which request expires (0 = no expiry)
bytes data; // Encoded function call
}GasSponsor.sol (source)
Manages gas fee subsidization with multi-layer constraints.
Constraint Layers:
| Layer | Constraint | Purpose |
|---|---|---|
| 1 | maxPerClaim |
Caps maximum single reimbursement |
| 2 | dailyLimitPerRelayer |
Prevents one relayer draining pool |
| 3 | dailyLimitPerUser |
Prevents Sybil-style abuse |
| 4 | globalDailyLimit |
Hard cap on total daily spending |
| 5 | Balance check | Cannot reimburse more than pool holds |
| 6 | Emergency pause | Owner can freeze all claims instantly |
Sponsorship Modes:
- Full —
maxPerClaim = 0.05 ETH(onboarding campaigns, local dev) - Partial —
maxPerClaim = 0.005 ETH(sustainable operations, Sepolia) - None — No GasSponsor deployed (relayer absorbs costs)
SampleToken.sol (source)
Meta-transaction-aware ERC-20 implementing the trusted forwarder pattern.
- Overrides
_msgSender()to extract real sender from forwarded calls - Uses assembly for gas-efficient sender extraction
- Mints 1M tokens to deployer for testing
- User's browser constructs a
ForwardRequeststruct - MetaMask's
eth_signTypedData_v4is called with EIP-712 typed data - Wallet shows human-readable signing UI (no gas, no TX)
- 65-byte signature is produced:
[r(32)][s(32)][v(1)] - Signature is sent to relayer over HTTP
// BatchExecutor verifies each signature (inlined for gas savings):
// 1. Deadline check (cheapest gate)
if (req.deadline != 0 && block.timestamp > req.deadline) { skip; }
// 2. EIP-712 hash + ecrecover
bytes32 structHash = keccak256(abi.encode(
REQUEST_TYPEHASH,
req.from, req.to, req.value, req.gas, req.nonce, req.deadline,
keccak256(req.data)
));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
address signer = _recoverSigner(digest, signature);
// 3. Verify signer and nonce
if (signer != req.from || req.nonce != nonces[req.from]) { skip; }
// 4. Increment nonce BEFORE execution (reentrancy guard)
nonces[req.from] = currentNonce + 1;
// 5. Execute with sender identity appended (ERC-2771 pattern)
req.to.call{gas: req.gas, value: req.value}(abi.encodePacked(req.data, req.from));{
name: "BatchExecutor",
version: "1",
chainId: 11155111, // Sepolia
verifyingContract: "0x..." // Deployed BatchExecutor address
}Relayer executes batch -> measures gas cost -> calls GasSponsor.claim()
|
+-- Cap: min(cost, maxPerClaim)
+-- Check relayer daily limit
+-- Check per-user daily limits
+-- Check global daily limit
+-- Check pool balance
+-- Transfer ETH to relayer
Relayers can call estimateReimbursement() (view function, no gas) before submitting a batch to verify the claim would succeed.
| Function | Access | Purpose |
|---|---|---|
setRelayer(addr, bool) |
Owner | Whitelist/revoke relayers |
setLimits(...) |
Owner | Update all constraint parameters |
setPaused(bool) |
Owner | Emergency pause/unpause |
emergencyWithdraw() |
Owner | Pull all funds immediately |
transferOwnership(addr) |
Owner | Transfer admin control |
The single-page application (index.html) provides:
- Wallet Connection — MetaMask integration with Sepolia network detection
- Status Dashboard — Real-time nonce, balance, and pending action counts
- Action Builder — Dynamic form to add/remove token transfers
- Gas Estimation — Live comparison of individual vs. batched costs
- Signature Flow — Step-by-step progress indicator
- Activity Log — Real-time logging of all operations
- Savings Visualization — Bar charts comparing gas costs
- Architecture Diagram — Interactive system architecture visualization
- Gas Analysis Table — Theoretical savings for various batch sizes
- Security Model — Trust guarantees and safety properties
- Dark/Light Theme — Persistent theme toggle
Each Ethereum TX pays 21,000 gas base overhead. For a simple ERC-20 transfer (~37,008 gas execution):
Real-world comparison includes the 21,000 base transaction cost that each individual transfer would pay separately:
| Batch Size | Individual Cost (N × 58,008) | Batched Cost (21K + internal) | Gas/Tx | Savings |
|---|---|---|---|---|
| 1 (direct) | 58,008 | 58,008 | 58,008 | -- |
| 2 transfers | 116,016 | 99,494 | 49,747 | 14% |
| 5 transfers | 290,040 | 134,242 | 26,848 | 54% |
| 10 transfers | 580,080 | 192,238 | 19,224 | 67% |
| 20 transfers | 1,160,160 | ~308,000 | ~15,400 | ~73% |
Batching saves gas at every batch size because the 21,000 base transaction cost is paid only once instead of N times. Savings asymptotically approach ~73% as batch size increases.
| Entity | Trust Boundary |
|---|---|
| User | Trusts their wallet, EIP-712 standard. Does NOT trust relayer. |
| BatchExecutor | Trusts cryptographic signatures. Does NOT trust relayer identity. |
| Relayer | Trusts contract code. Validates all requests before queuing. |
| GasSponsor | Trusts whitelisted relayers and owner. |
| Attack Vector | Mitigation |
|---|---|
| Replay (same chain) | Sequential per-user nonces |
| Cross-chain replay | EIP-712 domain includes chainId |
| Cross-contract replay | EIP-712 domain includes verifyingContract |
| Relayer censorship | Users can execute directly on-chain |
| Gas griefing | Per-call gas limits in ForwardRequest |
| Sponsor pool drain | 6-layer constraint system |
| Signature forgery | ECDSA + EIP-712 typed data |
| Reentrancy | Nonce incremented before execution |
| Malicious relayer | Can only execute pre-signed actions |
- Node.js v18+
- Foundry (
forge,cast,anvil) - MetaMask browser extension
- Sepolia ETH (from a faucet)
npm installFoundry dependencies (forge-std, openzeppelin-contracts) are in lib/ and managed via forge install.
Copy the example and fill in your keys:
cp .env.example .envSEPOLIA_RPC_URL=https://rpc.sepolia.org
DEPLOYER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY
RELAYER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY
# Etherscan API key (required for --verify during deployment)
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY
# Auto-populated after deployment
BATCH_EXECUTOR_ADDRESS=
SAMPLE_TOKEN_ADDRESS=
GAS_SPONSOR_ADDRESS=
RELAYER_ADDRESS=forge buildCompiles all Solidity contracts with optimizer (1000 runs, viaIR enabled, cancun EVM target). Artifacts go to out/.
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcast --verify
node script/post-deploy.jsOr use the npm shortcut:
npm run deploy:sepoliaThis will:
- Deploy BatchExecutor, SampleToken, and GasSponsor
- Whitelist the deployer as relayer in GasSponsor
- Write
deployment.jsonwith all contract addresses - Update
.envwith the deployed addresses
npm start- Push your repo to GitHub
- Connect the repo on Render.com as a Web Service
- Render auto-detects
render.yamland configures the service - Set the following environment variables in the Render dashboard:
SEPOLIA_RPC_URLDEPLOYER_PRIVATE_KEYRELAYER_PRIVATE_KEYBATCH_EXECUTOR_ADDRESSSAMPLE_TOKEN_ADDRESSGAS_SPONSOR_ADDRESSRELAYER_ADDRESS
- Set
CORS_ORIGINto your Render URL for production security
forge test -vv27 tests across 8 categories:
- Signature Verification — Valid EIP-712 signatures accepted; wrong-signer and wrong-nonce rejected
- Nonce Replay Protection & Recovery — Replays blocked;
incrementNonce()andincrementNonceBy()tested - Request Deadline / Expiry — Future deadlines accepted; past deadlines rejected;
deadline=0(no expiry) works - Batch Execution — Single-request and multi-request (3-tx) batches execute correctly
- Gas Sponsorship — Deposit, estimate, claim, cap enforcement, daily limits, and pause tested
- Gas Benchmark — Actual gas measured for batch sizes 2, 5, 10 vs. direct transfers
- Failure Handling — Empty batch, mismatched arrays, wrong nonce all revert correctly
- Partial Failure Resilience — Expired requests are skipped without killing the entire batch
forge test --gas-reportProduces a per-function gas breakdown:
| Contract | Function | Min | Avg | Median | Max |
|---|---|---|---|---|---|
| BatchExecutor | executeBatch |
22,430 | 100,809 | 94,085 | 218,938 |
| BatchExecutor | verify |
563 | 6,536 | 7,894 | 7,922 |
| BatchExecutor | DOMAIN_SEPARATOR |
223 | 223 | 223 | 223 |
| GasSponsor | claim |
26,762 | 119,390 | 165,705 | 165,705 |
| GasSponsor | deposit |
45,205 | 45,205 | 45,205 | 45,205 |
| SampleToken | transfer |
51,419 | 51,442 | 51,443 | 51,443 |
- Users have MetaMask installed and configured for Sepolia (chainId 11155111)
- Relayer has sufficient Sepolia ETH to pay gas upfront
- GasSponsor pool is funded before reimbursement claims
- Network gas prices are within reasonable testnet ranges
- Single relayer model (no multi-relayer coordination)
- Sequential nonces — Resolved:
incrementNonce()andincrementNonceBy(count)allow users to skip stuck nonces - Single relayer — No multi-relayer coordination or failover
- Testnet deployment — Deployed on Sepolia; not audited for mainnet
- Token-specific — SampleToken must be deployed with BatchExecutor as trusted forwarder; existing tokens need wrapper contracts
- MEV exposure — Batch transactions on mainnet could be sandwich-attacked; needs private mempool or Flashbots integration
- Day-based resets — GasSponsor daily limits use
block.timestamp / 1 days, which can vary
| Issue | Solution |
|---|---|
| Sequential nonces block queue | Added incrementNonce() and incrementNonceBy(count) to BatchExecutor |
| No test suite | Created 27-test Foundry suite covering all contract functionality |
| Theoretical-only gas estimation | forge test --gas-report provides per-function measurements |
| Legacy toolchain | Migrated to Foundry (forge/cast/anvil) for compilation, testing, and deployment |
| Local-only deployment | Added Sepolia testnet support and Render.com hosting |
- Multi-relayer support with nonce reservations
- L2 deployment (Arbitrum, Optimism) for further gas savings
- Session keys for one-click batch signing
- ERC-4337 Account Abstraction integration
- Private mempool submission for MEV protection
+-- contracts/ # Solidity smart contracts
| +-- BatchExecutor.sol # Core: EIP-712 verification, batch execution, nonce recovery
| +-- GasSponsor.sol # Gas sponsorship pool with 6-layer constraints
| +-- SampleToken.sol # Meta-tx-aware ERC-20 token
+-- test/ # Foundry test suite
| +-- GasBenchmark.t.sol # 27 tests: signatures, nonces, deadlines, batching, gas benchmarks
+-- script/ # Deployment scripts
| +-- Deploy.s.sol # Foundry deploy script (Solidity)
| +-- post-deploy.js # Updates .env and deployment.json after deploy
+-- lib/ # Foundry dependencies (git submodules)
| +-- forge-std/ # Foundry standard library
| +-- openzeppelin-contracts/ # OpenZeppelin contracts
+-- server.js # Express server (frontend + API + config)
+-- relayer.js # Batch queue, execution engine, gas history tracking
+-- signer.js # EIP-712 signing utilities
+-- index.html # Frontend HTML structure and layout
+-- app.js # Frontend application logic (wallet, signing, relay)
+-- styles.css # Frontend styling (dark/light theme support)
+-- foundry.toml # Foundry configuration (compiler, optimizer, networks)
+-- remappings.txt # Solidity import remappings
+-- render.yaml # Render.com deployment blueprint
+-- package.json # Node.js dependencies and npm scripts
+-- deployment.json # Deployed contract addresses (auto-generated)
+-- .env.example # Environment variable template
+-- ARCHITECTURE.md # Detailed system architecture design
+-- DEPLOYMENT.md # Step-by-step deployment guide
+-- SETUP.md # Setup verification checklist
+-- README.md # This file
Network: Sepolia testnet (chainId 11155111)
Smart Contract Toolchain: Foundry (forge 1.5.1, Solidity 0.8.24, optimizer 1000 runs + viaIR)
Server: Node.js + Express.js, deployable on Render.com
Frontend: Single-page HTML/JS using ethers.js v6
Local Server: npm start -> http://localhost:3000
- EIP-712: Typed Structured Data Hashing and Signing
- ERC-2771: Secure Protocol for Native Meta Transactions
- EIP-2028: Transaction Data Gas Cost Reduction
- ERC-4337: Account Abstraction Using Alt Mempool
- OpenZeppelin MinimalForwarder
- Gas Station Network (GSN)
- Ethereum Yellow Paper — Transaction Execution
- Foundry Book
- Ethers.js v6 Documentation
- iBatch Research Paper
