Appearance
Gasless Transactions Implementation
This guide explains how to implement gasless transactions in the Blockchain Voting System to hide blockchain complexity from users.
Why Gasless Transactions?
Gasless transactions eliminate the need for users to:
- Pay gas fees
- Own cryptocurrency
- Understand wallet mechanics
- Manage private keys directly
This creates a seamless experience similar to traditional web applications while maintaining blockchain benefits.
Implementation Approaches
1. Meta-Transactions (Recommended)
Meta-transactions allow users to sign transactions off-chain, which are then executed on-chain by a relayer that pays the gas fees.
2. Account Abstraction
Modern account abstraction solutions provide native gasless transaction support.
3. Backend Relayer
Your backend service signs and submits transactions on behalf of users.
Meta-Transaction Implementation
Architecture Overview
User Flow:
1. User signs vote data (off-chain)
2. Signature sent to backend
3. Backend validates signature
4. Backend submits transaction (pays gas)
5. Vote recorded on blockchain
Benefits:
- User never pays gas
- User retains control (signature required)
- Blockchain still immutableSmart Contract Changes
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract GaslessVoting {
using ECDSA for bytes32;
address public admin;
mapping(address => bool) public registeredVoters;
mapping(bytes32 => bool) public executedTransactions;
event VoteCast(address indexed voter, uint256 candidateId);
constructor(address _admin) {
admin = _admin;
}
function castVote(
uint256 electionId,
uint256 candidateId,
address voter,
bytes memory signature
) external {
// Create transaction hash
bytes32 transactionHash = keccak256(
abi.encodePacked(electionId, candidateId, voter, block.chainid)
);
// Prevent replay attacks
require(!executedTransactions[transactionHash], "Transaction already executed");
executedTransactions[transactionHash] = true;
// Verify signature
bytes32 ethSignedMessageHash = transactionHash.toEthSignedMessageHash();
address signer = ethSignedMessageHash.recover(signature);
require(signer == voter, "Invalid signature");
require(registeredVoters[voter], "Voter not registered");
// Process vote
_processVote(electionId, candidateId, voter);
emit VoteCast(voter, candidateId);
}
function _processVote(
uint256 electionId,
uint256 candidateId,
address voter
) internal {
// Actual vote processing logic
}
function registerVoter(address voter) external onlyAdmin {
registeredVoters[voter] = true;
}
}Frontend Implementation
typescript
import { ethers } from 'ethers'
class GaslessVotingService {
async signVote(
electionId: number,
candidateId: number,
wallet: ethers.Wallet
): Promise<{ signature: string; voter: string }> {
// Create transaction data
const transactionData = ethers.utils.solidityPack(
['uint256', 'uint256', 'address', 'uint256'],
[electionId, candidateId, wallet.address, await wallet.getChainId()]
);
// Hash the transaction data
const transactionHash = ethers.utils.keccak256(transactionData);
// Sign the hash
const signature = await wallet.signMessage(ethers.utils.arrayify(transactionHash));
return {
signature,
voter: wallet.address
};
}
async submitVote(
electionId: number,
candidateId: number,
signature: string,
voter: string
): Promise<boolean> {
try {
// Send to backend API
const response = await fetch('/api/vote/gasless', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
electionId,
candidateId,
signature,
voter
})
});
return response.ok;
} catch (error) {
console.error('Failed to submit gasless vote:', error);
return false;
}
}
}Backend Relayer Implementation
typescript
import { ethers } from 'ethers'
import { recoverAddress } from '@ethersproject/transactions'
class GaslessTransactionRelayer {
private provider: ethers.providers.JsonRpcProvider
private wallet: ethers.Wallet
private contract: ethers.Contract
constructor(
rpcUrl: string,
privateKey: string,
contractAddress: string,
contractAbi: any
) {
this.provider = new ethers.providers.JsonRpcProvider(rpcUrl)
this.wallet = new ethers.Wallet(privateKey, this.provider)
this.contract = new ethers.Contract(
contractAddress,
contractAbi,
this.wallet
)
}
async executeGaslessVote(
electionId: number,
candidateId: number,
voter: string,
signature: string
): Promise<string> {
try {
// Verify the signature matches the voter
const transactionData = ethers.utils.solidityPack(
['uint256', 'uint256', 'address', 'uint256'],
[electionId, candidateId, voter, await this.provider.getNetwork().chainId]
);
const transactionHash = ethers.utils.keccak256(transactionData);
const messageHash = ethers.utils.hashMessage(ethers.utils.arrayify(transactionHash));
const recoveredAddress = recoverAddress(messageHash, signature);
if (recoveredAddress.toLowerCase() !== voter.toLowerCase()) {
throw new Error('Signature does not match voter address');
}
// Execute the transaction
const tx = await this.contract.castVote(
electionId,
candidateId,
voter,
signature
);
// Wait for confirmation
await tx.wait();
return tx.hash;
} catch (error) {
console.error('Failed to execute gasless vote:', error);
throw error;
}
}
}Account Abstraction Approach
Using Thirdweb
typescript
import { ThirdwebSDK } from '@thirdweb-dev/sdk'
class AccountAbstractionVoting {
private sdk: ThirdwebSDK
constructor(walletPrivateKey: string, rpcUrl: string) {
this.sdk = new ThirdwebSDK(
new ethers.Wallet(walletPrivateKey),
{
gasless: {
openzeppelin: {
relayerUrl: "your-relayer-url",
relayerForwarderAddress: "relayer-forwarder-address"
}
}
}
)
}
async executeGaslessVote(
contractAddress: string,
electionId: number,
candidateId: number
) {
const contract = await this.sdk.getContract(contractAddress)
const tx = await contract.call("castVote", [electionId, candidateId])
return tx.receipt.transactionHash
}
}Using Biconomy
typescript
import { Biconomy } from '@biconomy/mexa'
class BiconomyGaslessVoting {
private biconomy: Biconomy
private web3: any
async initialize(provider: any, apiKey: string) {
this.biconomy = new Biconomy(provider, {
apiKey: apiKey,
debug: true
})
this.web3 = new Web3(this.biconomy)
return new Promise((resolve, reject) => {
this.biconomy.onEvent(this.biconomy.READY, () => resolve(true))
this.biconomy.onEvent(this.biconomy.ERROR, (error: any) => reject(error))
})
}
async executeGaslessVote(
contractAddress: string,
abi: any,
electionId: number,
candidateId: number,
from: string
) {
const contract = new this.web3.eth.Contract(abi, contractAddress)
const tx = contract.methods.castVote(electionId, candidateId)
const data = tx.encodeABI()
const transactionParams = {
from,
to: contractAddress,
data: data,
value: '0',
gasPrice: '0'
}
return await this.web3.eth.sendTransaction(transactionParams)
}
}Backend API Endpoint
typescript
// POST /api/vote/gasless
import { Request, Response } from 'express'
import { GaslessTransactionRelayer } from './relayer'
export class GaslessVoteController {
constructor(private relayer: GaslessTransactionRelayer) {}
async submitVote(req: Request, res: Response) {
try {
const { electionId, candidateId, signature, voter } = req.body
// Validate input
if (!electionId || !candidateId || !signature || !voter) {
return res.status(400).json({ error: 'Missing required fields' })
}
// Execute gasless transaction
const txHash = await this.relayer.executeGaslessVote(
electionId,
candidateId,
voter,
signature
)
res.json({
success: true,
transactionHash: txHash
})
} catch (error) {
console.error('Gasless vote failed:', error)
res.status(500).json({
success: false,
error: 'Failed to submit vote'
})
}
}
}Security Considerations
Signature Validation
- Always verify signatures match the claimed voter address
- Use proper message hashing (EIP-191 standard)
- Prevent replay attacks with transaction hashes
Relayer Security
- Secure private key storage
- Rate limiting to prevent abuse
- Transaction validation before execution
User Experience
- Clear messaging about what users are signing
- Progress indicators for transaction submission
- Error handling for failed transactions
Cost Management
Gas Sponsorship Strategies
- Subscription Model: Charge organizations for gas credits
- Usage-Based: Pay per transaction volume
- Hybrid: Free tier + paid scaling
Optimization Techniques
typescript
// Batch multiple operations
async batchOperations(votes: VoteData[]) {
const batch = new ethers.providers.JsonRpcBatchProvider(this.rpcUrl)
// Submit multiple votes in one transaction
}Testing Gasless Transactions
Unit Tests
typescript
describe('Gasless Voting', () => {
it('should validate signature correctly', async () => {
const wallet = ethers.Wallet.createRandom()
const service = new GaslessVotingService()
const voteData = {
electionId: 1,
candidateId: 5
}
const { signature, voter } = await service.signVote(
voteData.electionId,
voteData.candidateId,
wallet
)
// Verify signature is valid
expect(signature).toBeDefined()
expect(voter).toBe(wallet.address)
})
})Integration Tests
typescript
describe('Gasless Transaction Relayer', () => {
it('should execute gasless vote successfully', async () => {
const relayer = new GaslessTransactionRelayer(
process.env.RPC_URL!,
process.env.RELAYER_PRIVATE_KEY!,
CONTRACT_ADDRESS,
CONTRACT_ABI
)
const wallet = ethers.Wallet.createRandom()
const service = new GaslessVotingService()
const { signature, voter } = await service.signVote(1, 5, wallet)
const txHash = await relayer.executeGaslessVote(1, 5, voter, signature)
expect(txHash).toBeDefined()
expect(ethers.utils.isHexString(txHash)).toBe(true)
})
})Monitoring and Analytics
Transaction Tracking
typescript
class GaslessTransactionMonitor {
async trackTransaction(
txHash: string,
userId: string,
action: string
) {
// Log transaction for monitoring
await database.logTransaction({
txHash,
userId,
action,
timestamp: new Date(),
status: 'submitted'
})
}
async updateTransactionStatus(
txHash: string,
status: 'confirmed' | 'failed',
error?: string
) {
await database.updateTransactionStatus(txHash, status, error)
}
}Best Practices
- Always validate signatures before executing transactions
- Implement rate limiting to prevent abuse
- Use proper error handling for failed transactions
- Monitor gas usage and costs
- Secure private keys with proper key management
- Provide clear UX around what users are signing
- Implement replay protection with transaction hashes
This gasless transaction approach allows users to participate in blockchain voting without needing to understand wallets or pay gas fees, while still maintaining the security and immutability benefits of blockchain technology.