Gasless meta-transaction infrastructure with DAG-based batch execution, on-chain gas analytics, and configurable paymaster sponsorship. Built for the KRITI Hackathon at IIT Guwahati.
Solidity 0.8.24 | Foundry | OpenZeppelin v5.x | React 18 | TypeScript | MIT License
Every Ethereum transaction costs gas. For apps with frequent multi-step interactions, this means:
- Fees ranging from $1 to $50+ depending on congestion
- Multi-step flows like approve, deposit, stake require 2-3 separate transactions
- New users have to buy ETH before they can do anything
GasVote is a reusable gas optimization layer that any dApp can plug into. Users sign once, pay nothing, and the relayer handles submission and gas payment.
How it works:
| Technique | What it does |
|---|---|
| Transaction batching with DAG dependencies | Saves (N-1) x 21,000 base gas per batch |
| Meta-transactions (EIP-2771 / EIP-712) | Users sign once, pay zero gas |
| Configurable paymaster | 3 sponsorship modes with on-chain policy enforcement |
| On-chain analytics | Every batch's gas savings recorded and queryable |
| Dual-mode execution | Mode 1 (EIP-2771) + Mode 2 (Smart Account / EntryPoint) |
graph TB
subgraph User["User (Browser)"]
W[Wallet / MetaMask]
FE[React Frontend]
end
subgraph OffChain["Off-Chain"]
R[Relayer - Fastify + viem]
GO[Gas Oracle]
NM[Nonce Manager]
SSE[SSE Manager]
end
subgraph OnChain["On-Chain (EVM)"]
TF[TrustedForwarder]
BE[BatchExecutor]
PM[GasPaymaster]
MF[MetaTxBatchForwarder]
GA[GasAnalytics]
EP[MiniEntryPoint]
SA[SmartAccount]
DT[DemoToken]
DV[DemoVault]
DN[DemoNFT]
end
W -->|"EIP-712 sign"| FE
FE -->|"POST /relay"| R
FE -->|"SSE /relay/events"| SSE
R --> GO
R --> NM
R --> SSE
R -->|"Mode 1"| TF
R -->|"Mode 2"| EP
TF --> BE
BE --> DT & DV & DN
TF --> MF
MF --> PM
MF --> GA
EP --> SA
SA --> BE
PM -.->|reimburse| R
There are two execution paths. Mode 1 goes through the TrustedForwarder (EIP-2771) and requires target contracts to inherit ERC2771Context. Mode 2 goes through a MiniEntryPoint and SmartAccount, which works with any contract since the smart account acts as the caller.
| Mode 1 (EIP-2771) | Mode 2 (Smart Account) | |
|---|---|---|
| Path | User signs, relayer calls MetaTxBatchForwarder, then TrustedForwarder, then BatchExecutor | User signs, relayer calls MiniEntryPoint, then SmartAccount, then BatchExecutor |
| Identity | _msgSender() via ERC2771Context |
Owner-based verification |
| Target requirement | Must inherit ERC2771Context | Works with anything |
| Trade-off | Strong on-chain user identity | No contract modification needed |
The demo flow: user picks actions (approve GVDT, deposit into vault, stake), frontend bundles them into one EIP-712 message, user signs once in MetaMask (free), relayer validates and submits, BatchExecutor runs the calls with dependency checks, paymaster reimburses relayer, GasAnalytics records savings. The user pays $0 gas — all costs are sponsored by the paymaster.
gasvote/
contracts/ Foundry project (Solidity 0.8.24)
src/
core/ 7 core contracts (BatchExecutor, TrustedForwarder, etc.)
demo/ DemoToken (ERC-20 + Permit), DemoVault, DemoNFT (ERC-721)
interfaces/ 7 interfaces
libraries/ Types, Events, Errors, TransientReentrancyGuard
test/ 148 tests (unit, integration, fuzz, gas, halmos, invariant)
script/ DeployAll.s.sol, DemoMode2.s.sol
relayer/ Fastify + viem relay service
src/
index.ts Server entry
config.ts Zod-validated env config
client.ts viem public + wallet clients
types.ts Request/response types
services/ gasOracle, nonceManager, relay, signature, sseManager
routes/ 11 REST + SSE endpoints
abi/ Contract ABIs
frontend/ React + Vite + wagmi
src/
App.tsx Router (4 routes + Infra sub-tabs)
pages/ 7 pages (Dashboard, BatchBuilder, BundlerDemo, Infra, Analytics, PaymasterAdmin, GasCalculator)
components/ 10 components
hooks/ useGasAnalytics, useRelayer, usePaymaster, useGasPrice
lib/ wagmi config, relayer API, contracts, utils
abi/ 9 ABIs
setup-local.ps1 One-click local setup (Windows)
setup-local.sh One-click local setup (Linux/macOS)
Makefile Build/test/deploy shortcuts
pnpm-workspace.yaml Workspaces: relayer, frontend
You need Node.js >= 20, pnpm >= 9, and Foundry.
Install Foundry:
# Linux / macOS
curl -L https://foundry.paradigm.xyz | bash
foundryup# Windows - download from https://github.com/foundry-rs/foundry/releases
# Add to PATH in every new PowerShell session:
$env:Path = "$env:USERPROFILE\.foundry\bin;$env:Path"The setup script installs deps, compiles contracts, starts Anvil, deploys, writes .env files, and transfers test tokens.
# Windows
git clone https://github.com/<your-username>/gasvote.git
cd gasvote
.\setup-local.ps1# Linux / macOS
git clone https://github.com/<your-username>/gasvote.git
cd gasvote
chmod +x setup-local.sh
./setup-local.shWhen done, open two more terminals:
cd relayer && pnpm dev # http://localhost:3001
cd frontend && pnpm dev # http://localhost:5173Open http://localhost:5173 and skip to the "Connect wallet" section below.
- Clone and build:
git clone https://github.com/<your-username>/gasvote.git
cd gasvote
pnpm install
cd contracts && forge build && cd ..- Start Anvil (keep running):
pnpm anvil- Deploy and configure:
pnpm deploy:anvilCopy the printed addresses into both .env files (or use the pre-filled Anvil templates):
cp relayer/.env.anvil relayer/.env
cp frontend/.env.anvil frontend/.envTransfer test tokens to account #1 so the frontend shows a balance:
cast send 0x0165878A594ca255338adfa4d48449f69242Eb8F "transfer(address,uint256)(bool)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 100000000000000000000000 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://127.0.0.1:8545- Start relayer + frontend:
pnpm dev:relayer # http://localhost:3001
pnpm dev:frontend # http://localhost:5173Add the Anvil network to MetaMask: RPC URL http://127.0.0.1:8545, Chain ID 31337.
Import test account #1 (not #0, that's the relayer):
Private key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
This account has 10,000 ETH and 100,000 GVDT tokens.
Go to Batch Builder, enter an amount like 100, click Execute Batch, sign the EIP-712 message in MetaMask (free), and watch the steps complete. Check Dashboard and Analytics for results.
| Variable | Description | Example |
|---|---|---|
| VITE_RELAYER_URL | Relayer HTTP endpoint | http://localhost:3001 |
| VITE_CHAIN_ID | Target chain ID | 31337 (Anvil) or 11155111 (Sepolia) |
| VITE_RPC_URL | JSON-RPC URL | http://127.0.0.1:8545 |
| VITE_WALLETCONNECT_PROJECT_ID | WalletConnect project ID (optional) | demo |
| VITE_BATCH_EXECUTOR | BatchExecutor contract address | 0x... |
| VITE_TRUSTED_FORWARDER | TrustedForwarder contract address | 0x... |
| VITE_GAS_PAYMASTER | GasPaymaster contract address | 0x... |
| VITE_GAS_ANALYTICS | GasAnalytics contract address | 0x... |
| VITE_METATX_BATCH_FORWARDER | MetaTxBatchForwarder address | 0x... |
| VITE_MINI_ENTRY_POINT | MiniEntryPoint contract address | 0x... |
| VITE_SMART_ACCOUNT | SmartAccount contract address | 0x... |
| VITE_DEMO_TOKEN | DemoToken (GVDT) contract address | 0x... |
| VITE_DEMO_VAULT | DemoVault contract address | 0x... |
| Variable | Description | Example / Default |
|---|---|---|
| RPC_URL | JSON-RPC URL | http://127.0.0.1:8545 |
| CHAIN_ID | Target chain ID | 31337 |
| RELAYER_PRIVATE_KEY | Relayer wallet private key | 0xac09... (Anvil account #0) |
| PORT | HTTP listen port | 3001 (local) / 8080 (Railway) |
| META_TX_BATCH_FORWARDER | MetaTxBatchForwarder address | 0x... |
| TRUSTED_FORWARDER | TrustedForwarder address | 0x... |
| GAS_PAYMASTER | GasPaymaster address | 0x... |
| MINI_ENTRY_POINT | MiniEntryPoint address | 0x... |
| GAS_ANALYTICS | GasAnalytics address | 0x... |
| MAX_GAS_PRICE_GWEI | Max gas price before rejecting relays | 100 |
| ENABLE_GAS_QUEUEING | Queue relays when gas is high | false |
| CORS_ORIGINS | Comma-separated allowed origins | http://localhost:5173 |
| Variable | Description | Required for |
|---|---|---|
| SEPOLIA_RPC_URL | Sepolia RPC endpoint | Sepolia deploy |
| DEPLOYER_PRIVATE_KEY | Deployer wallet private key | Sepolia deploy |
| ETHERSCAN_API_KEY | Etherscan API key for verify | Sepolia deploy |
Template files: .env.example (root), frontend/.env.example, frontend/.env.anvil, frontend/.env.sepolia, relayer/.env.example, relayer/.env.anvil, relayer/.env.sepolia.
Run all 148 tests:
pnpm test:contractsRun specific tests:
cd contracts
forge test --match-path test/unit/BatchExecutor.t.sol -vvv
forge test --match-test testBatchExecuteWithDependencies -vvv
forge test --match-path test/integration/*.t.sol -vvvGas reports:
pnpm test:gas
pnpm snapshotTest breakdown (148 tests across 13 suites):
| Suite | Tests | What it covers |
|---|---|---|
| BatchExecutor | 18 | DAG execution, dependency resolution, failure isolation, gas measurement |
| TrustedForwarder | 11 | EIP-712 sigs, nonces, deadlines, replay protection |
| GasPaymaster | 25 | 3 sponsorship modes, policy, deposit/withdraw, auth |
| MetaTxBatchForwarder | 19 | Atomic flow: verify, paymaster, execute, analytics |
| GasAnalytics | 8 | Recording, aggregates, authorization |
| MiniEntryPoint | 17 | UserOp validation, smart account exec, paymaster |
| SmartAccount | 11 | Owner auth, batch exec, entry point, unauthorized rejection |
| DemoTokenVault | 14 | ERC-20, permit, vault ops, ERC-2771 |
| Integration | 9 | E2E Mode 1 + Mode 2 |
| BatchExecutor (fuzz) | 5 | Random DAGs, cascading failures (256 runs) |
| GasPaymaster (invariant) | 3 | Pool accounting invariants (128 runs x 64 depth) |
| GasComparison | 1 | Gas: baseline vs Mode 1 vs Mode 2 |
| BatchExecutor (symbolic) | 7 | Halmos: DAG validity, skip semantics |
Security properties verified: replay attacks revert, tampered calldata reverts, expired deadlines revert, wrong signer reverts, unauthorized reimburse/analytics/execute reverts, dependency failures cascade correctly, exhausted paymaster rejects.
Local Anvil:
pnpm anvil # Terminal 1
pnpm deploy:anvil # Terminal 2Sepolia testnet -- get Sepolia ETH from a faucet, an RPC URL from Alchemy/Infura, and an Etherscan API key:
export SEPOLIA_RPC_URL="https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY"
export DEPLOYER_PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
export ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
pnpm deploy:sepoliaDeployAll.s.sol deploys all 10 contracts in one transaction and wires permissions automatically: authorizes callers on the paymaster, whitelists demo contracts, deposits 1 ETH, sets up analytics recorders, mints 1M GVDT to the deployer.
7 core contracts:
| Contract | What it does |
|---|---|
| BatchExecutor | Multicall with DAG dependencies, failure isolation, per-call gas measurement |
| TrustedForwarder | EIP-2771 + EIP-712 forwarder, batch support, nonce management, deadlines |
| GasPaymaster | 3 modes (full sponsor, partial, ERC-20 payment), on-chain policy |
| MetaTxBatchForwarder | Coordinates: verify sig, check paymaster, execute batch, record analytics |
| GasAnalytics | On-chain gas tracking, global aggregates |
| MiniEntryPoint | ERC-4337-inspired entry point for Mode 2 |
| SmartAccount | Minimal contract wallet, owner auth + batch execution |
3 demo contracts: DemoToken (ERC-20 + EIP-2612 Permit + ERC-2771, symbol GVDT), DemoVault (deposit/stake/withdraw), DemoNFT (ERC-721 + ERC-2771, max 10k supply).
EIP stack: EIP-712 (structured signing), EIP-2771 (trusted forwarder), EIP-2612 (permit), ERC-4337 (paymaster + entry point), EIP-1559 (gas oracle), EIP-1153 (transient storage).
Fastify 5, viem 2, TypeScript, Zod validation, pino logging.
| Method | Path | What it does |
|---|---|---|
| POST | /relay/mode1 | Relay via TrustedForwarder (EIP-2771) |
| POST | /relay/mode2 | Relay via MiniEntryPoint (ERC-4337) |
| POST | /relay/bundle | Bundle multiple Mode 1 requests |
| POST | /relay/session | Create SSE session (returns UUID) |
| GET | /relay/events/:id | SSE event stream |
| GET | /status/:txHash | Transaction status + receipt |
| GET | /gas-price | EIP-1559 gas prices |
| GET | /nonce/:address | User nonce |
| GET | /analytics | On-chain stats |
| GET | /recent-batches | Persisted batch log (max 200) |
| GET | /health | Health check + relayer balance |
The relayer supports SSE for real-time progress. Create a session, open the event stream, then relay with the X-SSE-Session header. The frontend's RelayerTerminal component renders these as a color-coded terminal.
cd relayer
pnpm dev # http://localhost:3001
pnpm build # production buildReact 18, Vite 6, wagmi v2, TailwindCSS 3, Recharts, TanStack Query v5.
4 main routes (Infra contains 3 sub-tabs):
- User pages: Dashboard (stats, gas breakdown, recent activity), Batch Builder (DAG builder, faucet, phantom comparison, terminal), Bundler Demo (multi-user bundling with real EIP-712 signing)
- Infra pages (tabbed): Analytics (break-even simulator, gas benchmarks), Paymaster Admin (deposit/withdraw, policies, contracts), Gas Calculator (slider-based savings projector)
10 components: Layout, DAGVisualizer, RelayerTerminal, PhantomComparison, GasSavingsChart, SponsorRunway, BatchStep, StatCard, ConnectWallet, RecentActivity.
cd frontend
pnpm dev # http://localhost:5173
pnpm build # production buildFrom forge test --match-contract GasComparisonTest -vv on Cancun EVM. Scenario: permit, deposit, stake (3 DeFi ops).
| Method | Exec gas | Intrinsic | Total | vs baseline |
|---|---|---|---|---|
| Baseline (3 individual txs) | 160,964 | 63,000 | 223,964 | -- |
| Mode 1 (MetaTxBatchForwarder) | 401,733 | 21,000 | 422,733 | +89% |
| Mode 2 (MiniEntryPoint) | 314,689 | 21,000 | 335,689 | +50% |
For a small 3-operation batch, the meta-tx overhead (signature verification, nonce management, paymaster checks, analytics recording, ERC-2771 forwarding) exceeds the 42k gas saved from combining 3 txs into 1. This is expected.
The real value is: users pay $0, one signature instead of three, atomic execution with dependency tracking, no ETH needed to get started, and savings improve as batch size grows.
See GAS_REPORT.md for the full report.
| Command | What it does |
|---|---|
| pnpm build:contracts | Compile (dev profile) |
| pnpm test:contracts | Run all 148 tests |
| pnpm test:gas | Tests with gas report |
| pnpm deploy:anvil | Deploy to local Anvil |
| pnpm deploy:sepolia | Deploy to Sepolia + verify |
| pnpm anvil | Start Anvil node |
| pnpm dev:relayer | Start relayer (dev) |
| pnpm dev:frontend | Start frontend (dev) |
| pnpm fmt:sol | Format Solidity |
| pnpm snapshot | Generate gas snapshot |
On Linux/macOS you can also use make build, make test, make deploy-anvil, etc. Windows users should stick with pnpm commands.
Design assumptions:
- Single relayer: the system assumes one trusted relayer per deployment. Relay decentralization (multi-relayer selection, reputation, staking) is out of scope.
- Anvil determinism: gas measurements in GAS_REPORT.md are deterministic on Anvil / Cancun EVM. Mainnet numbers will differ due to base fee dynamics, warm storage from previous transactions, and validator MEV behavior.
- Batch size: the BatchExecutor supports up to 32 calls per batch. DAG depth is uncapped but gas limits impose a practical ceiling of ~15-20 non-trivial calls per block.
- ERC-2771 requirement: Mode 1 requires target contracts to inherit
ERC2771Context. Unmodified third-party contracts must use Mode 2 (SmartAccount) instead. - No ERC-20 gas payment oracle: the GasPaymaster's ERC-20 payment mode uses a fixed tokenGasPrice set by the owner. There is no on-chain price oracle integration.
Known limitations:
- 3-op batches cost more, not less: for the demo's 3-operation batch (approve + deposit + stake), the meta-tx overhead (signature verification, nonce management, paymaster checks, analytics recording) exceeds the 42k gas saved from combining 3 txs into 1. Break-even is ~8-10+ operations per batch.
- Smart account deployment: Mode 2 SmartAccount addresses are deterministic but deployment is not counterfactual. The account must be deployed before use (DeployAll handles this).
- No multi-chain: contracts are deployed per-chain. Cross-chain batching and bridging are not supported.
- Relayer censorship: the relayer can refuse to submit a valid request (censorship), but cannot forge, modify, or replay user-signed operations.
- Demo contracts only: DemoToken, DemoVault, and DemoNFT are minimal implementations for demonstration. They are not intended for production use.
The project implements defense-in-depth across the contract stack:
- Replay protection: 2D nonce scheme (Mode 1) and sequential nonces (Mode 2), both chain-bound via EIP-712 domain separators. Every request includes a deadline timestamp.
- Reentrancy: all state-changing functions use
TransientReentrancyGuard(EIP-1153), saving ~5,000 gas per guard entry compared to OpenZeppelin's storage-based approach. - Access control:
Ownable2Stepfor ownership. Paymaster reimbursement restricted to authorized callers. Analytics recording restricted to authorized recorders. - Signature validation: ECDSA recovery with zero-address checks. Invalid, expired, or replayed signatures revert atomically.
- Trust model: users are untrusted (all actions require valid EIP-712 signatures), the relayer is semi-trusted (can censor but not forge), the deployer/owner is fully trusted.
This is an educational / hackathon project and has not been professionally audited. See SECURITY.md for the full threat model, access control matrix, and formal verification details.
forge/anvil/cast not found -- add Foundry to PATH. On Windows: $env:Path = "$env:USERPROFILE\.foundry\bin;$env:Path". On Linux/macOS: add export PATH="$HOME/.foundry/bin:$PATH" to your shell config.
Anvil connection refused -- make sure Anvil is running in a separate terminal (pnpm anvil).
"Could not load EIP-712 domain" -- contract addresses in frontend/.env are wrong or missing. Re-paste from deploy output.
Relay fails or simulation errors -- check relayer/.env has correct addresses, make sure the test user has GVDT tokens, restart the relayer after changing .env.
Dashboard shows all zeros -- you need to execute a batch first via Batch Builder.
Wallet not connecting -- MetaMask needs the Anvil network (chain ID 31337, RPC http://127.0.0.1:8545), and you need to import account #1.
Windows make not found -- use pnpm commands instead (pnpm test:contracts, pnpm deploy:anvil).
Need to start over -- stop all terminals, re-run setup-local.ps1 (Windows) or setup-local.sh (Linux/macOS), then start relayer and frontend again.