This system implements a Gas Fee Optimizer and Batch Transaction System that leverages three core techniques from Ethereum gas optimization research:
- Transaction Batching — Amortizing the 21,000 base gas cost across multiple operations
- Meta-Transactions (EIP-712) — Off-chain signing with on-chain relayed execution
- Gas Sponsorship — Optional subsidization of gas fees with configurable constraints
┌─────────────────────────────────────────────────────────────────────┐
│ SYSTEM ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ EIP-712 ┌───────────┐ Batch TX ┌──────┐ │
│ │ User / │───(sign off───▶│ Relayer │───(on-chain)──▶│ Batch│ │
│ │ Wallet │ chain) │ Server │ │Execu-│ │
│ │(MetaMask)│ │ (Node.js) │ │ tor │ │
│ └──────────┘ └─────┬─────┘ └──┬───┘ │
│ │ │ │ │
│ │ │ Reimburse │ │
│ │ ▼ │ │
│ │ ┌───────────┐ │ │
│ │ │ Gas │ │ │
│ │ │ Sponsor │◀──(call target)───┘ │
│ │ │ Pool │ │
│ │ └───────────┘ │
│ │ │
│ │ Token Transfer ┌───────────┐ │
│ └──────────────────────▶│ Sample │ │
│ (via BatchExecutor) │ Token │ │
│ │ (ERC-20) │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Role: Central hub that verifies EIP-712 signatures, enforces nonce-based replay protection, and executes batched calls to target contracts.
┌─────────────────────────────────────────────────────────┐
│ BatchExecutor.sol │
├─────────────────────────────────────────────────────────┤
│ │
│ State: │
│ DOMAIN_SEPARATOR (bytes32) — EIP-712 domain hash │
│ REQUEST_TYPEHASH (bytes32) — struct type hash │
│ nonces (mapping) — per-user nonce counter │
│ minBatchSize (uint256) — iBatch MinX policy │
│ owner (address) — admin address │
│ │
│ Core Functions: │
│ verify(req, sig) → bool │
│ ├─ Check deadline (0 = no expiry) │
│ ├─ Hash request via EIP-712 encoding │
│ ├─ Recover signer via ecrecover │
│ └─ Check signer == req.from && nonce match │
│ │
│ executeBatch(requests[], signatures[]) → bool[] │
│ ├─ Check min batch size (max enforced off-chain) │
│ ├─ For each (request, signature): │
│ │ ├─ Check deadline (skip if expired) │
│ │ ├─ Inline verify (skip if invalid) │
│ │ ├─ Increment nonce (pre-execution) │
│ │ └─ Execute: req.to.call(req.data ++ req.from) │
│ └─ Emit BatchExecuted event │
│ │
│ Nonce Recovery: │
│ incrementNonce() — Skip 1 stuck nonce │
│ incrementNonceBy(n) — Skip n stuck nonces (1-50) │
│ │
│ Security: │
│ - EIP-712 domain binding (chain + contract) │
│ - Sequential nonce enforcement │
│ - Sender identity appended to calldata │
│ - Pre-execution nonce increment (reentrancy guard) │
│ - Batch deadline prevents stale request execution │
│ - Graceful partial failures (skip, don't revert) │
│ │
└─────────────────────────────────────────────────────────┘
Design Decisions:
- Nonce model: Sequential per-user nonces (like Ethereum itself). Prevents replay attacks while maintaining ordering guarantees.
- Sender propagation: The original sender (
req.from) is appended to calldata as the last 20 bytes. Target contracts extract it to identify the real user. - Gas isolation: Each sub-call has its own gas limit (
req.gas), preventing one failed call from consuming the entire batch's gas.
Role: Manages a fund pool that reimburses relayers for gas spent executing user transactions. Implements configurable constraints to prevent abuse.
┌─────────────────────────────────────────────────────────┐
│ GasSponsor.sol │
├─────────────────────────────────────────────────────────┤
│ │
│ Sponsorship Modes: │
│ Full — 100% reimbursement up to cap │
│ Partial — Capped at maxPerClaim (relayer absorbs │
│ the excess) │
│ │
│ Constraint Layers (defense-in-depth): │
│ Layer 1: Per-Claim Cap (maxPerClaim) │
│ Layer 2: Per-Relayer Daily (dailyLimitPerRelayer) │
│ Layer 3: Per-User Daily (dailyLimitPerUser) │
│ Layer 4: Global Daily (globalDailyLimit) │
│ Layer 5: Pool Balance Check │
│ Layer 6: Emergency Pause (owner-controlled) │
│ │
│ Claim Flow: │
│ relayer.claim(gasAmount, users[]) │
│ ├─ Cap: min(amount, maxPerClaim) │
│ ├─ Check pool balance (early exit saves gas) │
│ ├─ Check relayer daily limit (reset if new day) │
│ ├─ Check global daily limit │
│ ├─ Check per-user limits (split equally) │
│ ├─ Update all tracking counters │
│ └─ Transfer ETH to relayer │
│ │
└─────────────────────────────────────────────────────────┘
Design Decisions:
- Day-based resets: Using
block.timestamp / 1 daysfor daily limit tracking. Gas-efficient compared to sliding windows. - Equal split accounting: Per-user costs are divided equally among batch participants for simplicity and fairness.
- Whitelisted relayers: Only approved relayers can claim, preventing unauthorized drain.
- Estimate function:
estimateReimbursement()allows relayers to pre-check viability before submitting a batch.
Role: Standard ERC-20 token that recognizes forwarded calls from BatchExecutor and extracts the real sender.
┌─────────────────────────────────────────────────────────┐
│ SampleToken.sol │
├─────────────────────────────────────────────────────────┤
│ │
│ Inherits: OpenZeppelin ERC20 │
│ │
│ Meta-Tx Awareness: │
│ _msgSender() override: │
│ IF caller == trustedForwarder AND data >= 20 bytes │
│ THEN sender = last 20 bytes of calldata │
│ ELSE sender = msg.sender (normal flow) │
│ │
│ This pattern allows: │
│ - Direct calls (user pays gas) → normal ERC-20 │
│ - Relayed calls (via BatchExecutor) → meta-tx │
│ │
└─────────────────────────────────────────────────────────┘
Role: Collects signed requests from users, queues them, and periodically submits batches on-chain. Optionally claims gas reimbursement.
┌─────────────────────────────────────────────────────────────────┐
│ Relayer Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Express Server (server.js) │
│ ├─ GET / → Serve frontend │
│ ├─ GET /health → Health check + relayer status │
│ ├─ GET /api/config → Serve contract addresses from .env │
│ ├─ POST /api/relay → Accept signed requests │
│ ├─ GET /api/batch/status → Queue status │
│ ├─ GET /api/gas-stats → Gas usage analytics & history │
│ ├─ GET /api/nonce/:addr → On-chain nonce for address │
│ ├─ POST /api/batch/flush → Force flush queue (admin) │
│ └─ POST /api/faucet → Mint test tokens to wallet │
│ │
│ Relayer Engine (relayer.js) │
│ ├─ Request Queue → pendingRequests[], pendingSignatures[] │
│ ├─ Auto-Flush Timer → Submits batch every N seconds │
│ ├─ Min Batch Size → iBatch MinX policy (configurable) │
│ ├─ Max Batch Size → Flushes immediately when queue is full │
│ ├─ Pre-Filter → Verifies batch before submission │
│ ├─ Nonce Sync → Rejects stale nonces before queuing │
│ └─ Retry Logic → Re-queues failed requests │
│ │
│ Signer Utility (signer.js) │
│ ├─ buildRequest() → Construct ForwardRequest struct │
│ ├─ signRequest() → EIP-712 wallet signing │
│ └─ signBatchRequests() → Sign multiple with nonce mgmt │
│ │
└─────────────────────────────────────────────────────────────────┘
User (Browser) Relayer (Server) Blockchain (Sepolia)
│ │ │
[1] │── Connect Wallet ──────┼─────────────────────────│
│ │ │
[2] │── Build Actions ───────┼─────────────────────────│
│ (to, amount) × N │ │
│ │ │
[3] │── Sign EIP-712 ────────┼─────────────────────────│
│ (MetaMask popup) │ │
│ No gas paid! │ │
│ │ │
[4] │── POST /api/relay ────▶│ │
│ {request, signature} │ │
│ │ │
[5] │ [Queue Request] │
│ [Wait for batch] │
│ │ │
[6] │ │── executeBatch() ──────▶│
│ │ (single TX) │
│ │ [Verify sigs]
│ │ [Check nonces]
│ │ [Execute calls]
│ │ [Emit events]
│ │◀── TX receipt ──────────│
│ │ │
[7] │ │── claim() ─────────────▶│ (GasSponsor)
│ │ (reimbursement) │
│ │◀── ETH transfer ────────│
│ │ │
[8] │◀── Result ─────────────│ │
│ (txHash, savings) │ │
| Step | Actor | Action | Gas Cost |
|---|---|---|---|
| 1 | User | Connect MetaMask to Sepolia | 0 |
| 2 | User | Define N token transfers in the UI | 0 |
| 3 | User | Sign N ForwardRequests via EIP-712 | 0 |
| 4 | User | Send signed requests to relayer API | 0 |
| 5 | Relayer | Queue requests, wait for batch window | 0 |
| 6 | Relayer | Submit single executeBatch() TX |
~21K + N×17K gas |
| 7 | Relayer | Claim reimbursement from GasSponsor | ~45K gas |
| 8 | User | Receive confirmation and savings report | 0 |
Total user gas cost: 0 ETH (relayer pays, optionally reimbursed by sponsor pool)
Every Ethereum transaction has a fixed base cost of 21,000 gas regardless of what it does. When users send N individual transactions:
With batching, there's only one base cost:
The savings are:
Where
Measured using forge test --gas-report (Solidity 0.8.24, optimizer 1000 runs, viaIR). Individual cost includes the 21,000 base transaction cost per transfer:
| Batch Size | Individual Cost (N × 58,008) | Batched Cost (21K + internal) | Savings | Savings % |
|---|---|---|---|---|
| 1 | 58,008 | 58,008 | 0 | 0% |
| 2 | 116,016 | 99,494 | 16,522 | 14% |
| 5 | 290,040 | 134,242 | 155,798 | 54% |
| 10 | 580,080 | 192,238 | 387,842 | 67% |
| 20 | 1,160,160 | ~308,000 | ~852,000 | ~73% |
The savings converge toward ~73% as batch size increases, following:
Per EIP-2028, calldata costs:
- 4 gas per zero byte
- 16 gas per non-zero byte
A ForwardRequest struct in calldata is ~256 bytes. For N requests:
This is factored into the overhead but remains small relative to the 21,000 base cost saved per additional request.
| Entity | Trusts | Does NOT Trust |
|---|---|---|
| User | Their own wallet, EIP-712 standard | Relayer (signs specific actions only) |
| BatchExecutor | EIP-712 signatures, nonce ordering | Relayer identity (verifies everything) |
| Relayer | BatchExecutor contract code | User requests (validates before queuing) |
| GasSponsor | Whitelisted relayers, owner | Non-whitelisted addresses |
Attack │ Mitigation
────────────────────────────────┼──────────────────────────────────────
Replay attack (same chain) │ Sequential nonces per user
Cross-chain replay │ EIP-712 domain includes chainId
Cross-contract replay │ EIP-712 domain includes contract addr
Relayer censorship │ Users can execute directly on-chain
Gas griefing (relayer) │ Per-call gas limits in ForwardRequest
Sponsor pool drain │ 6-layer constraint system
Signature forgery │ ECDSA + EIP-712 typed data
Front-running │ Nonces enforce ordering
Reentrancy │ Nonce incremented before execution
Malicious relayer │ Can only execute what user signed
-
Non-Custodial: The relayer never holds user funds or private keys. It can only execute pre-signed, specific actions.
-
Censorship Resistant: If the relayer refuses to submit a transaction, users can call
executeBatch()directly (paying their own gas). -
Replay Protected: The combination of (chainId, contractAddress, userNonce) creates a unique context for every signature.
-
Fail-Safe Gas Sponsorship: The 6-layer constraint system (per-claim, per-relayer, per-user, global, balance, pause) ensures the sponsor pool cannot be drained even if a relayer is compromised.
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("BatchExecutor"), // Contract name
keccak256("1"), // Version
block.chainid, // 11155111 (Sepolia)
address(this) // Deployed contract address
));bytes32 structHash = keccak256(abi.encode(
REQUEST_TYPEHASH,
req.from, req.to, req.value, req.gas, req.nonce,
req.deadline, // Request expiry (0 = no expiry)
keccak256(req.data) // Dynamic bytes are hashed
));
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", // EIP-712 prefix
DOMAIN_SEPARATOR, // Domain context
structHash // Message content
));User signs typed data in MetaMask
│
▼
signature = sign(digest) → 65 bytes: [r(32)][s(32)][v(1)]
│
▼
On-chain: ecrecover(digest, v, r, s) → recovered_address
│
▼
Check: recovered_address == request.from ?
Check: request.nonce == nonces[request.from] ?
Check: request.deadline == 0 || block.timestamp <= request.deadline ?
│
▼
If all pass → execute the request
Ideal for local dev with Anvil. The dApp operator funds the pool generously:
maxPerClaim: 0.05 ETH (covers most batch txs)
dailyLimitPerRelayer: 1 ETH (relayer can claim freely)
dailyLimitPerUser: 0.01 ETH (~20 free actions/day)
globalDailyLimit: 5 ETH (caps total daily spending)
User experience: Completely gasless. User signs, relayer executes, pool pays.
For Sepolia deployment. Lower limits to conserve testnet ETH:
maxPerClaim: 0.005 ETH (covers ~50% of cost)
dailyLimitPerRelayer: 0.1 ETH
dailyLimitPerUser: 0.002 ETH
globalDailyLimit: 0.5 ETH
User experience: Still gasless from user's perspective, but limits are tighter.
Relayer pays gas out of pocket (e.g., funded by service fees):
No GasSponsor contract deployed.
Relayer absorbs all gas costs as a business expense.
| Factor | Current Design | Scaling Path |
|---|---|---|
| Batch size | 10 (configurable) | Up to ~50 before block gas limit concerns |
| Queue strategy | Time-based + size-based flush | Priority queues, fee markets |
| Multi-relayer | Single relayer | Relayer rotation, load balancing |
| Storage | On-chain nonces, off-chain queue | L2 bridging, state channels |
| Cross-token | Single token (SampleToken) | Any ERC-20 with trusted forwarder pattern |
| Approach | Gas Saving | User UX | Complexity | This System |
|---|---|---|---|---|
| Direct TX | 0% (baseline) | Must hold ETH | Low | ✗ |
| OpenZeppelin Forwarder | ~10% (single relay) | Gasless signing | Medium | Inspired by |
| Multicall (batching only) | ~40-60% | Still pays gas | Low | Combines with |
| GSN (Gas Station Network) | ~10% | Gasless | High | Simplified version |
| This System | ~60-70% | Gasless + batched | Medium | ✓ |
The key differentiator is combining batching (cost reduction) with meta-transactions (UX improvement) and optional sponsorship (onboarding enablement) in a single cohesive system.
User Frontend Server/Relayer BatchExecutor GasSponsor
│ │ │ │ │
│──Connect──────▶│ │ │ │
│ │──getNonce()───────────────────────────▶│ │
│ │◀──nonce────────────────────────────────│ │
│ │ │ │ │
│──Add Actions──▶│ │ │ │
│ (to,amount) │ │ │ │
│ │ │ │ │
│──Click Sign───▶│ │ │ │
│ │──signTypedData()──▶│(MetaMask) │ │
│◀──Approve─────▶│ │ │ │
│ │◀──signature────────│ │ │
│ │ │ │ │
│ │──POST /api/relay──▶│ │ │
│ │ │──verify()────────▶│ │
│ │ │◀──true────────────│ │
│ │ │ │ │
│ │ │ [Queue + Wait] │ │
│ │ │ │ │
│ │ │──executeBatch()──▶│ │
│ │ │ │──verify each──│
│ │ │ │──execute each─│
│ │ │ │──emit events──│
│ │ │◀──receipt─────────│ │
│ │ │ │ │
│ │ │──claim()──────────────────────────▶│
│ │ │◀──ETH reimbursement───────────────│
│ │ │ │ │
│ │◀──Result───────────│ │ │
│◀──Display──────│ │ │ │
│ savings │ │ │ │
-
Trusted Forwarder Pattern (ERC-2771): BatchExecutor appends the real sender to calldata; target contracts extract it via
_msgSender()override. -
EIP-712 Structured Data Signing: Human-readable, domain-bound signatures that wallets like MetaMask render clearly to users.
-
Nonce-Based Replay Protection: Sequential nonces per user, incremented before execution to prevent reentrancy-based nonce reuse.
-
Defense-in-Depth Sponsorship: Multiple independent constraint layers ensure no single point of failure can drain the pool.
-
Queue-and-Flush Pattern: Off-chain request aggregation with configurable time/size flush triggers for optimal batching.
-
Graceful Degradation: If the relayer is down, users can submit transactions directly to BatchExecutor (paying their own gas).