DEV Community

Cover image for πŸ›‘οΈ Day 19 of #30DaysOfSolidity β€” Signature-Based Web3 Authentication for Private Events
Saurav Kumar
Saurav Kumar

Posted on

πŸ›‘οΈ Day 19 of #30DaysOfSolidity β€” Signature-Based Web3 Authentication for Private Events

🎯 Introduction

Welcome to Day 19 of #30DaysOfSolidity!

In this tutorial, we’ll build a signature-based Web3 authentication system β€” a gas-efficient, secure, and scalable solution for private Ethereum events, token-gated workshops, and VIP meetups.

Instead of maintaining an on-chain whitelist, the organizer signs invitations off-chain, and attendees verify their entry on-chain using smart contract verification. This approach mirrors real-world event workflows: off-chain approval, on-chain authentication.

By the end of this guide, you’ll understand how to implement Web3 authentication with Solidity and Foundry, minimizing gas costs while maintaining full security.


βš™οΈ Tech Stack

Layer Technology
Smart Contract Solidity (^0.8.20)
Testing & Deployment Foundry
Off-chain Signer Node.js + Ethers.js
Frontend Demo React + Ethers.js
Network Ethereum / EVM-compatible

Absolutely! We can add a file structure section to the blog to make it more developer-friendly and professional. Here’s the updated section for your Dev.to blog with proper structure placement:


πŸ“ Project File Structure

Here’s the recommended file structure for the Signature-Based Web3 Authentication project:

signature-gate/ β”‚ β”œβ”€ contracts/ β”‚ └─ SignatureGate.sol # Smart contract for signature-based entry β”‚ β”œβ”€ scripts/ β”‚ └─ signer.js # Node.js script for organizer to sign invites β”‚ β”œβ”€ frontend/ β”‚ β”œβ”€ src/ β”‚ β”‚ β”œβ”€ components/ β”‚ β”‚ β”‚ └─ ClaimEntry.jsx # React component to claim entry β”‚ β”‚ β”œβ”€ SignatureGateABI.json # ABI generated from contract β”‚ β”‚ └─ App.jsx # Main React app β”‚ └─ package.json # Frontend dependencies β”‚ β”œβ”€ test/ β”‚ └─ SignatureGate.t.sol # Foundry test file for contract β”‚ β”œβ”€ foundry.toml # Foundry configuration └─ README.md # Project README 
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή Explanation

  • contracts/ β€” Contains Solidity smart contracts; SignatureGate.sol implements the verification logic.
  • scripts/ β€” Off-chain scripts for signing messages (signer.js).
  • frontend/ β€” React app demonstrating on-chain claim functionality with Ethers.js.
  • test/ β€” Foundry tests for signature validation, replay protection, and expiry checks.
  • foundry.toml β€” Configuration for Foundry deployments and testing.
  • README.md β€” Project documentation.

πŸ’‘ Core Concept: Signature-Based Web3 Authentication

Traditional token-gated events require storing on-chain whitelists. This is costly and cumbersome.

With signature-based entry, we only store the used signatures on-chain. Here’s how it works:

  1. Organizer signs an invite off-chain including:
  • Attendee address
  • Event ID
  • Expiry timestamp
  • Unique nonce
  1. Attendee receives the signed message (via email, QR, or backend API).

  2. Attendee submits the signature on-chain by calling claim().

  3. Contract verifies the signature using ecrecover:

  • Confirms the signer is the organizer
  • Checks for expiration
  • Ensures the signature hasn’t been reused

βœ… If valid, the attendee is granted entry, and the contract emits an EntryGranted event.


🧩 Smart Contract β€” SignatureGate.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SignatureGate { address public organizer; mapping(bytes32 => bool) public used; event EntryGranted(address indexed attendee, uint256 indexed eventId, uint256 nonce); event OrganizerChanged(address oldOrganizer, address newOrganizer); error InvalidSignature(); error SignatureExpired(); error SignatureAlreadyUsed(); error NotOrganizer(); constructor(address _organizer) { require(_organizer != address(0), "organizer zero"); organizer = _organizer; } function setOrganizer(address _new) external { if (msg.sender != organizer) revert NotOrganizer(); emit OrganizerChanged(organizer, _new); organizer = _new; } function claim( uint256 eventId, uint256 expiry, uint256 nonce, bytes calldata sig ) external { if (expiry != 0 && block.timestamp > expiry) revert SignatureExpired(); bytes32 digest = _hashForSigning(msg.sender, eventId, expiry, nonce); if (used[digest]) revert SignatureAlreadyUsed(); address signer = _recover(digest, sig); if (signer != organizer) revert InvalidSignature(); used[digest] = true; emit EntryGranted(msg.sender, eventId, nonce); } function hashForSigning( address attendee, uint256 eventId, uint256 expiry, uint256 nonce ) external pure returns (bytes32) { return _hashForSigning(attendee, eventId, expiry, nonce); } function _hashForSigning( address attendee, uint256 eventId, uint256 expiry, uint256 nonce ) internal pure returns (bytes32) { bytes32 raw = keccak256(abi.encodePacked("\x19Event Entry:\n", attendee, eventId, expiry, nonce)); return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", raw)); } function _recover(bytes32 digest, bytes memory sig) internal pure returns (address) { if (sig.length != 65) return address(0); bytes32 r; bytes32 s; uint8 v; assembly { r := mload(add(sig, 0x20)) s := mload(add(sig, 0x40)) v := byte(0, mload(add(sig, 0x60))) } if (v < 27) v += 27; return ecrecover(digest, v, r, s); } } 
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ How It Works

  • organizer is the signer.
  • claim() verifies the signature, expiry, and nonce.
  • used[digest] prevents replay attacks.
  • EntryGranted confirms successful verification.

πŸ§ͺ Off-Chain Signing β€” Node.js Script

The organizer generates signatures for attendees:

import { ethers } from "ethers"; // node signer.js <PK> <ATTENDEE> <EVENT_ID> <EXPIRY> <NONCE> async function main() { const [pk, attendee, eventId, expiry, nonce] = process.argv.slice(2); const wallet = new ethers.Wallet(pk); const abiPacked = ethers.utils.solidityPack( ["string", "address", "uint256", "uint256", "uint256"], ["\x19Event Entry:\n", attendee, eventId, expiry, nonce] ); const raw = ethers.utils.keccak256(abiPacked); const signature = await wallet.signMessage(ethers.utils.arrayify(raw)); console.log("Signature:", signature); } main(); 
Enter fullscreen mode Exit fullscreen mode

πŸ’» Frontend Demo β€” React + Ethers.js

import React, { useState } from "react"; import { ethers } from "ethers"; import SignatureGateABI from "./SignatureGateABI.json"; export default function ClaimEntry({ contractAddress }) { const [eventId, setEventId] = useState(""); const [expiry, setExpiry] = useState(""); const [nonce, setNonce] = useState(""); const [sig, setSig] = useState(""); const [status, setStatus] = useState(""); async function claimEntry() { const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); const contract = new ethers.Contract(contractAddress, SignatureGateABI, signer); try { const tx = await contract.claim(eventId, expiry, nonce, sig); setStatus("Transaction sent: " + tx.hash); await tx.wait(); setStatus("βœ… Entry confirmed!"); } catch (err) { setStatus("❌ Error: " + err.message); } } return ( <div> <h3>Claim Event Entry</h3> <input placeholder="Event ID" onChange={(e) => setEventId(e.target.value)} /> <input placeholder="Expiry (Unix)" onChange={(e) => setExpiry(e.target.value)} /> <input placeholder="Nonce" onChange={(e) => setNonce(e.target.value)} /> <input placeholder="Signature" onChange={(e) => setSig(e.target.value)} /> <button onClick={claimEntry}>Submit</button> <p>{status}</p> </div> ); } 
Enter fullscreen mode Exit fullscreen mode

πŸ”’ Security Considerations

Concern Mitigation
Replay attacks Nonce + signature hash stored
Expired invites Expiry timestamp
Key compromise setOrganizer() allows rotation
Gas costs Only store successful claim hashes
Privacy Off-chain approvals, no public whitelist

Pro tip: For production, use EIP-712 typed signatures for wallet-friendly structured signing.


πŸš€ Deployment Guide

  1. Deploy contract:
forge create src/SignatureGate.sol:SignatureGate --constructor-args <organizer_address> 
Enter fullscreen mode Exit fullscreen mode
  1. Sign invites with the Node.js script.
  2. Guests submit signatures via the React frontend.
  3. The contract verifies and emits entry confirmation events.

🌐 Use Cases

  • Token-gated events & workshops
  • DAO community meetups
  • NFT VIP access
  • KYC-free gated dApps
  • Decentralized conference check-ins

🏁 Summary

This signature-based Web3 authentication system is gas-efficient, secure, and real-world ready.
It provides off-chain invitation flexibility with on-chain verification, making it perfect for private Ethereum events.

β€œEfficient, secure, and decentralized β€” the future of Web3 event access control.”

Top comments (0)