Introduction
In the world of blockchain, gas fees can be a significant barrier to user adoption. What if users could interact with your decentralized application without paying gas fees? This is exactly what we’ve built - a Gas-Sponsored Voting dApp on the Conflux Network that allows users to vote on proposals completely free of charge, thanks to Conflux’s innovative gas sponsorship mechanism.
This project demonstrates how to build a fully functional voting system where the contract owner sponsors all transaction fees, making it accessible to users regardless of their CFX balance. Let’s dive into the architecture, implementation, and key features of this dApp.
Video Tutorial: https://youtu.be/f53TPiqhMyE?si=kIBl2LFNTMHNHxGI
GitHub Repo: https://github.com/Vikash-8090-Yadav/cfxGassponsor
Project Overview
The Conflux Voting dApp is a Next.js-based decentralized application that enables:
- Creating voting proposals (owner-only)
- Voting on proposals (anyone can vote)
- Real-time vote tracking and visualization
- Gas-free transactions for all users
- Seamless wallet integration with Fluent Wallet
Architecture
The project follows a modern full-stack architecture:
┌─────────────────┐
│ Next.js App │ (Frontend - React/TypeScript)
│ (Frontend) │
└────────┬────────┘
│
│ js-conflux-sdk
│
┌────────▼────────┐
│ Conflux RPC │ (Blockchain Network)
│ Node │
└────────┬────────┘
│
┌────────▼────────┐
│ Smart Contract │ (Solidity - GasSponsoredVoting.sol)
│ on Conflux │
└─────────────────┘
Smart Contract Implementation
The heart of our dApp is the GasSponsoredVoting smart contract. Let’s examine its key components:
Contract Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasSponsoredVoting {
struct Proposal {
string title;
string description;
uint256 yesVotes;
uint256 noVotes;
bool isActive;
mapping(address => bool) hasVoted;
}
address public owner;
Proposal[] public proposals;
event ProposalCreated(uint256 indexed proposalId, string title);
event VoteCast(uint256 indexed proposalId, address indexed voter, bool vote);
Key Design Decisions:
- Struct-based Proposals: Each proposal contains title, description, vote counts, active status, and a mapping to track who has voted
- Owner Pattern: Only the contract owner can create proposals, ensuring controlled governance
- Event Emissions: Events are emitted for proposal creation and voting, enabling efficient off-chain tracking
Creating Proposals
function createProposal(string memory _title, string memory _description) public onlyOwner {
Proposal storage newProposal = proposals.push();
newProposal.title = _title;
newProposal.description = _description;
newProposal.yesVotes = 0;
newProposal.noVotes = 0;
newProposal.isActive = true;
emit ProposalCreated(proposals.length - 1, _title);
}
This function:
- Can only be called by the contract owner (enforced by
onlyOwnermodifier) - Creates a new proposal with initial vote counts set to zero
- Automatically activates the proposal
- Emits an event for frontend synchronization
Voting Mechanism
function vote(uint256 _proposalId, bool _vote) public {
require(_proposalId < proposals.length, "Invalid proposal ID");
Proposal storage proposal = proposals[_proposalId];
require(proposal.isActive, "Proposal is not active");
require(!proposal.hasVoted[msg.sender], "Already voted");
proposal.hasVoted[msg.sender] = true;
if (_vote) {
proposal.yesVotes++;
} else {
proposal.noVotes++;
}
emit VoteCast(_proposalId, msg.sender, _vote);
}
Security Features:
-
One Vote Per User: The
hasVotedmapping prevents double voting - Active Proposal Check: Users can only vote on active proposals
- Bounds Checking: Validates proposal ID exists before accessing
View Functions
The contract provides several view functions for reading data:
function getProposal(uint256 _proposalId) public view returns (
string memory title,
string memory description,
uint256 yesVotes,
uint256 noVotes,
bool isActive
) {
require(_proposalId < proposals.length, "Invalid proposal ID");
Proposal storage proposal = proposals[_proposalId];
return (
proposal.title,
proposal.description,
proposal.yesVotes,
proposal.noVotes,
proposal.isActive
);
}
function hasVoted(uint256 _proposalId, address _voter) public view returns (bool) {
require(_proposalId < proposals.length, "Invalid proposal ID");
return proposals[_proposalId].hasVoted[_voter];
}
function getProposalCount() public view returns (uint256) {
return proposals.length;
}
These functions enable the frontend to:
- Display all proposal details
- Check if a user has already voted
- Get the total number of proposals
Frontend Implementation
The frontend is built with Next.js 14, React, TypeScript, and Tailwind CSS. It uses the js-conflux-sdk to interact with the Conflux blockchain.
Wallet Integration
The dApp integrates with Fluent Wallet, Conflux’s native wallet:
const connectWallet = async () => {
try {
if (typeof window === 'undefined') {
alert('Please open in a browser')
return
}
// Check for Fluent Wallet
if (window.conflux && window.conflux.enable) {
try {
const accounts = await window.conflux.enable()
if (accounts && accounts.length > 0) {
setAccount(accounts[0])
// Update Conflux instance to use wallet provider
if (cfx) {
cfx.provider = window.conflux
}
alert('Wallet connected successfully!')
}
} catch (error: any) {
console.error('Error enabling wallet:', error)
alert(`Failed to connect wallet: ${error.message || 'User rejected the request'}`)
}
} else {
alert('Please install Fluent Wallet extension!')
}
} catch (error: any) {
console.error('Error connecting wallet:', error)
alert(`Failed to connect wallet: ${error.message || 'Unknown error'}`)
}
}
Contract Loading
The contract is loaded dynamically using the Conflux SDK:
const loadContract = async () => {
if (!cfx || !contractAddress) {
alert('Please enter contract address')
return
}
let normalizedAddress = contractAddress.trim()
try {
setLoading(true)
const contractInstance = cfx.Contract({
abi: CONTRACT_ABI,
address: normalizedAddress,
})
// Test if contract is accessible
const count = await contractInstance.getProposalCount()
console.log('Contract loaded successfully. Proposal count:', count.toString())
setContract(contractInstance)
await loadProposals(contractInstance)
alert(`Contract loaded successfully! Found ${count.toString()} proposal(s).`)
} catch (error: any) {
console.error('Error loading contract:', error)
alert(`Failed to load contract: ${error.message || 'Invalid contract address or network error'}`)
} finally {
setLoading(false)
}
}
Creating Proposals
When the owner creates a proposal, the transaction is sent through the wallet:
const createProposal = async () => {
if (!contract || !newProposal.title || !newProposal.description) {
alert('Please fill in all fields')
return
}
if (!account || !cfx) {
alert('Please connect your wallet first')
return
}
try {
setLoading(true)
// Use the wallet provider to send transaction
const tx = contract.createProposal(newProposal.title, newProposal.description)
const txHash = await tx.sendTransaction({ from: account })
console.log('Transaction sent:', txHash)
// Wait for transaction to be mined
let receipt = null
while (!receipt) {
receipt = await cfx.getTransactionReceipt(txHash)
if (!receipt) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
console.log('Transaction receipt:', receipt)
setNewProposal({ title: '', description: '' })
await loadProposals()
alert('Proposal created successfully!')
} catch (error: any) {
console.error('Error creating proposal:', error)
alert(error.message || 'Failed to create proposal')
} finally {
setLoading(false)
}
}
Voting Functionality
Voting is straightforward - users click Yes or No, and the transaction is processed:
const vote = async (proposalId: number, voteValue: boolean) => {
if (!contract || !account || !cfx) {
alert('Please connect your wallet first')
return
}
try {
setLoading(true)
const tx = contract.vote(proposalId, voteValue)
const txHash = await tx.sendTransaction({ from: account })
console.log('Transaction sent:', txHash)
// Wait for transaction to be mined
let receipt = null
while (!receipt) {
receipt = await cfx.getTransactionReceipt(txHash)
if (!receipt) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
console.log('Transaction receipt:', receipt)
await loadProposals()
alert(`Vote ${voteValue ? 'Yes' : 'No'} recorded!`)
} catch (error: any) {
console.error('Error voting:', error)
alert(error.message || 'Failed to vote')
} finally {
setLoading(false)
}
}
Loading Proposals
The frontend loads all proposals and checks voting status:
const loadProposals = async (contractInstance?: any) => {
const contractToUse = contractInstance || contract
if (!contractToUse) return
try {
setLoading(true)
const count = await contractToUse.getProposalCount()
const proposalCount = parseInt(count.toString())
const proposalsData: Proposal[] = []
for (let i = 0; i < proposalCount; i++) {
const proposal = await contractToUse.getProposal(i)
let voted = false
if (account) {
voted = await contractToUse.hasVoted(i, account)
}
proposalsData.push({
id: i,
title: proposal[0],
description: proposal[1],
yesVotes: parseInt(proposal[2].toString()),
noVotes: parseInt(proposal[3].toString()),
isActive: proposal[4],
hasVoted: voted,
})
}
setProposals(proposalsData)
} catch (error) {
console.error('Error loading proposals:', error)
} finally {
setLoading(false)
}
}
User Interface
The UI is built with Tailwind CSS and features:
- Wallet Connection Section: Connect Fluent Wallet with one click
- Contract Loading: Enter contract address and load proposals
- Proposal Creation: Owner can create new proposals with title and description
-
Voting Interface:
- Visual vote counts (Yes/No)
- Progress bars showing vote distribution
- Disabled voting buttons if user already voted
- Real-time updates after voting
Key UI Components
<div className="mb-4">
<div className="flex justify-between mb-2">
<span className="text-green-600 font-semibold">Yes: {proposal.yesVotes}</span>
<span className="text-red-600 font-semibold">No: {proposal.noVotes}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full"
style={{
width: proposal.yesVotes + proposal.noVotes > 0
? `${(proposal.yesVotes / (proposal.yesVotes + proposal.noVotes)) * 100}%`
: '0%'
}}
/>
</div>
</div>
This creates a visual progress bar showing the percentage of “Yes” votes.
Deployment Process
1. Contract Compilation
npm run compile
This uses Hardhat to compile the Solidity contract and generate artifacts.
2. Deployment Script
The deployment script (scripts/deploy-conflux.js) handles contract deployment:
const { Conflux } = require('js-conflux-sdk');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
async function main() {
const conflux = new Conflux({
url: process.env.NEXT_PUBLIC_CONFLUX_RPC_URL || 'https://test.confluxrpc.com',
networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID || '1'),
});
const account = conflux.wallet.addPrivateKey(process.env.PRIVATE_KEY);
console.log('Deploying from account:', account.address);
const artifactPath = path.join(__dirname, '../artifacts/contracts/Voting.sol/GasSponsoredVoting.json');
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
const bytecode = artifact.bytecode;
const abi = artifact.abi;
const contract = conflux.Contract({ abi, bytecode });
console.log('Deploying contract...');
const receipt = await contract.constructor().sendTransaction({
from: account.address,
}).executed();
const contractAddress = receipt.contractCreated;
console.log('\n✅ GasSponsoredVoting deployed successfully!');
console.log('Contract Address:', contractAddress);
console.log('\n📝 Add this to your .env file:');
console.log(`NEXT_PUBLIC_CONTRACT_ADDRESS=${contractAddress}`);
}
3. Deploy to Testnet
npm run deploy:testnet
This deploys the contract and outputs the contract address for use in the frontend.
Gas Sponsorship Setup
The magic of this dApp is that users don’t pay gas fees. This is achieved through Conflux’s SponsorWhitelistControl mechanism.
How Gas Sponsorship Works
- Sponsor Storage: The contract owner deposits CFX to sponsor storage costs
- Sponsor Gas: The owner sets a gas sponsor balance
-
Whitelist: The owner adds the zero address (
0x0000000000000000000000000000000000000000) to the whitelist, allowing anyone to use the contract without paying gas
Setting Up Sponsorship
- Navigate to ConfluxScan Contract Tool
- Connect your wallet
-
Sponsor Storage: Call
setSponsorForCollateralwith your contract address and deposit CFX (e.g., 50 CFX) -
Sponsor Gas: Call
setSponsorForGaswith your contract address and set the upper bound (e.g.,1000000000000000Drip = 10^15 Drip) -
Add Whitelist: Call
addPrivilegeByAdminwith:- Contract address
- Array:
["0x0000000000000000000000000000000000000000"]
After these steps, all users can interact with your contract without paying gas fees!
Key Features
1. Gas-Free Transactions
Users can vote without holding CFX, removing a major barrier to adoption.
2. One Vote Per User
The smart contract enforces that each address can only vote once per proposal, ensuring fair voting.
3. Real-Time Updates
The frontend automatically refreshes after transactions, showing updated vote counts immediately.
4. Owner Controls
Only the contract owner can create proposals, providing governance control.
5. Proposal Management
Proposals can be closed by the owner, preventing further voting on completed proposals.
6. Visual Feedback
Progress bars and color-coded vote counts make it easy to see voting results at a glance.
Technology Stack
- Frontend: Next.js 14, React 18, TypeScript
- Styling: Tailwind CSS
- Blockchain: Conflux Network
- Smart Contracts: Solidity 0.8.20
- Development: Hardhat
- SDK: js-conflux-sdk
- Wallet: Fluent Wallet
Project Structure
cfxGassponsor/
├── app/
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main voting interface
├── contracts/
│ └── Voting.sol # Smart contract
├── scripts/
│ └── deploy-conflux.js # Deployment script
├── hardhat.config.js # Hardhat configuration
├── next.config.js # Next.js configuration
├── tailwind.config.js # Tailwind configuration
└── package.json # Dependencies
Environment Variables
Create a .env file with:
PRIVATE_KEY=your_private_key_here
NEXT_PUBLIC_CONTRACT_ADDRESS=cfxtest:...
NEXT_PUBLIC_CONFLUX_RPC_URL=https://test.confluxrpc.com
NEXT_PUBLIC_NETWORK_ID=1
Conclusion
This Gas-Sponsored Voting dApp demonstrates the power of Conflux Network’s gas sponsorship feature. By removing gas fees for end users, we’ve created a more accessible and user-friendly decentralized application. The combination of a secure smart contract, modern frontend framework, and seamless wallet integration creates a production-ready voting system.
Resources
- Conflux Network Documentation
- Fluent Wallet
- ConfluxScan
- js-conflux-sdk Documentation
- Hardhat Documentation
Happy Building! 