DEV Community

Cover image for # How I Built a Fully Decentralized On-Chain Game with 0 Lines of Code, Thanks to Gemini
crow
crow

Posted on

# How I Built a Fully Decentralized On-Chain Game with 0 Lines of Code, Thanks to Gemini

My nickname is crow, and a few months ago, I was an indie developer with what I'd call junior-level skills. Today, I'm the creator of a fully-functional, decentralized, on-chain game called Musical Chairs. The twist? I didn't write a single line of the production code myself. 100% of it was generated by Gemini, my AI coding partner integrated into VS Code.

This isn't just a story about a cool project; it's a story about a new way of building. It's about how a single person with a clear vision can leverage AI to execute complex technical tasks, from writing secure smart contracts to deploying a multi-container production environment.

The Idea: Decentralization First

The concept was simple: take the childhood game of Musical Chairs and bring it to the blockchain. A game of pure reaction, provably fair, where the winner takes the pot.

My initial thought was to use a stablecoin like USDT for the game's currency. It seemed user-friendly. However, as Gemini and I delved into the technicals, I discovered a fundamental conflict with my vision. The USDT smart contract is controlled by a central entity, Tether, which has the technical ability to pause or freeze any wallet. This "kill switch" functionality, while understandable from their perspective, was a deal-breaker for me. The core of my project was to be truly decentralized.

This led to my first major pivot: the game would use the native currency of the chain (ETH on Arbitrum). This not only ensured complete decentralization—where no single entity could interfere with player funds—but also simplified the smart contract logic significantly. To account for price volatility, the owner can adjust the stake amount as needed.

The High-Level Architecture

The application is composed of three main pillars, all orchestrated within a Docker environment.

  1. Smart Contract (Solidity): The heart of the game. It acts as the unstoppable and transparent source of truth, handling player stakes, game state transitions, and prize distribution. Through a proxy pattern, it provides a stable, immutable address and state for users, while allowing the owner to securely upgrade the underlying game logic.
  2. Backend (Go): The brains of the operation. It manages the game lifecycle, listens for blockchain events, and communicates with players in real-time via WebSockets. It's the off-chain coordinator for the on-chain action.
  3. Frontend (HTML/CSS/JS): The face of the game. A simple, lightweight client that interacts with the user's wallet (like MetaMask) and communicates with the backend.

Here's how they interact:

  • A user connects their wallet on the Frontend.
  • The Frontend talks to the Backend via a REST API to get game configuration and via WebSockets for real-time updates (e.g., other players joining).
  • The Backend listens to the blockchain for events from the Smart Contract (like deposits).
  • The Backend sends transactions to the Smart Contract to manage the game (e.g., starting the music round).

The Docker Ecosystem

To run this in production, we containerized everything. This makes deployment, scaling, and management incredibly robust.

  • nginx_reverse_proxy: The entry point. It handles SSL, serves the frontend, and routes API/WebSocket traffic.
  • backend: The main Go application.
  • keyservice: A dedicated, hardened microservice whose only job is to sign blockchain transactions.
  • postgres: The database for storing game history and analytics data.
  • fail2ban: An intrusion prevention service that monitors logs and bans malicious IPs.
  • umami: A self-hosted, privacy-respecting analytics service. markdown
  • logrotate: A key privacy-enforcing service. It's configured to rotate Nginx logs daily while keeping zero old log files (rotate 0). This ensures that sensitive information like IP addresses is purged from the server in less than 24 hours, maximizing user anonymity.

Deep Dive: The Smart Contract

The smart contract is the most critical piece of the puzzle. Security, reliability, and transparency were non-negotiable. Here’s how we achieved that.

Upgradability and Safety

We used OpenZeppelin's UUPS (Universal Upgradeable Proxy Standard). This allows the contract logic to be upgraded without losing the contract's state (i.e., ongoing games, funds). It's a battle-tested pattern for long-term projects.

A key security measure is the _disableInitializers() call in the implementation contract's constructor:

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}
Enter fullscreen mode Exit fullscreen mode

This prevents anyone from calling the initialize function on the logic contract itself, which could otherwise be a vector for hijacking. Interestingly, this line had to be commented out during testing with tools like Echidna and Foundry, as they would fail, but it's crucial for production security.

Security Features

  • Re-entrancy Guard: We use OpenZeppelin's ReentrancyGuard to protect all functions that handle fund transfers (depositStake, claimWinnings, etc.) from re-entrancy attacks.
  • Ownership and Role Separation: We implemented a three-address system to separate concerns and minimize risk:
    • Owner (Cold Wallet): This address has the highest level of control (upgrading the contract, changing fees). It was generated offline and is never exposed to the internet. Transactions are signed on an air-gapped machine, and the raw signed transaction is then broadcast using a tool like Arbiscan's "Broadcast Transaction" page.
    • Backend (Hot Wallet): This address handles the day-to-day operations, like starting games and recording results. It can be replaced instantly by the owner if compromised, without a timelock, allowing for rapid response.
    • Commission Recipient: A dedicated address that can only receive platform commissions. This separation ensures that even if the hot wallet is compromised, the core contract and its funds remain secure. In the future, I'm considering moving the owner role to a 2-of-3 multisig for even greater resilience.
  • Timelocks for Critical Functions: Functions that could move significant funds, like emergencyWithdraw, are protected by a timelock. A withdrawal is first proposed with a specific amount, and can only be executed after a delay. This gives users full transparency and time to react if they see something they don't like.
  • Zero Address Protection: All functions that set addresses (like changing the owner or backend wallet) prevent setting the address to 0x0, which would permanently "brick" the contract.

Gas Optimization

Gemini helped me implement several gas optimization techniques. While modern compilers are excellent, explicit optimizations are still key:

  • Using Custom Errors: Instead of require() with string messages, we use custom errors (error InsufficientStake();). This saves significant gas on deployment and during runtime when a check fails.
  • Efficient State Management: We carefully designed data structures to minimize writes to storage, which is the most expensive operation on the EVM. For example, we read values into memory, perform operations, and then write the final result back to storage once.
  • Unchecked Arithmetic: For operations where we are certain underflow/overflow cannot occur (e.g., incrementing a counter after checking its bounds), we use unchecked { ++i; } blocks to save the gas that would be spent on the default safety checks in Solidity 0.8+.

Rigorous Testing and Verification

A smart contract is only as good as its testing. We were exhaustive:

  • Unit & Fuzz Testing: We wrote 81 unit tests with Hardhat and Foundry, achieving near-100% code coverage. We also wrote fuzz tests to throw thousands of random inputs at the functions.
  • Invariant Testing: We used Echidna to run 50,000 random transactions against the contract to test for broken invariants (e.g., "the contract balance should never be less than the sum of all player deposits"). No vulnerabilities were found.
  • Custom Attack Contracts: We wrote ReentrancyAttacker.sol and RevertingReceiver.sol to simulate specific attack scenarios and ensure our guards worked as expected.
  • Static Analysis: The code was analyzed with Slither and Solhint, and the bytecode was checked with Mythril.
  • Gas Reporting: We used hardhat-gas-reporter to analyze the gas cost of every function, helping us pinpoint areas for optimization.
  • Verification: The contracts are verified on Sourcify. This provides cryptographic proof that the deployed bytecode matches the open-source code. We initially planned to use Arbiscan, but our deployment coincided with Etherscan's major transition from their V1 API to the new, unified V2 keys. This transitional period caused temporary verification issues, making Sourcify an excellent and reliable alternative.

This multi-layered approach to security and testing gives me, and hopefully my users, a high degree of confidence in the contract's integrity.

In the next part, I'll dive into the Backend, Frontend, and the operational infrastructure that powers the game.

Now, let's get into the off-chain machinery that brings the game to life: the microservices, the security fortress I built around them, and the path forward.

Deep Dive: The Keyservice Microservice - A Digital Fortress

One of my biggest concerns was handling the backend's private key. This key is "hot" – it needs to be online to sign transactions like starting a game. A compromise here would be disastrous. My solution was to build a dedicated, hardened microservice with a single responsibility: signing transactions.

It's a tiny Go application, but it's built like a fortress:

  • Isolation: It runs in its own Docker container and does nothing but receive data from the main backend, sign it, and return the signature. It has no other network access.
  • Docker Secrets: The encrypted private key JSON and its password are not in the container image or environment variables. They are mounted as Docker Secrets, which are stored in-memory on the host and are only accessible to the services they're granted to. The files on the host machine have their permissions locked down with chmod 600.
  • Quantum-Resistant Encryption: This is where my paranoia really kicked in. I didn't just encrypt the secrets; I used GPG with AES-256 and a high s2k-count (--s2k-mode 3 --s2k-count 65011712). This is a slow, synchronous encryption method that makes brute-force attacks computationally infeasible, even against future threats like Grover's algorithm for quantum computers. This is military-grade stuff.
  • The "Dead Man's Switch": What if the keyservice container crashes and Docker fails to restart it? The main backend has a unique, obfuscated module containing the GPG-encrypted key, passphrase, and docker-compose.yml file. If it can't reach the keyservice, it uses a master password to decrypt these assets in-memory, restart the container, and then securely wipes the decrypted files from disk by overwriting them with zeros. It's an automated disaster recovery plan.

I considered hardware keys like a YubiKey or cloud HSMs, but rejected them. A physical key introduces a single point of failure and a potential de-anonymization vector. Cloud HSMs require trusting a third party, which I wasn't willing to do. This self-contained, heavily fortified microservice was the answer.

Future Hardening: The next step is to move from Docker Compose to Kubernetes for more granular control and to "harden" the containers using seccomp and AppArmor.

  • Seccomp (Secure Computing Mode) is a Linux kernel feature that restricts the system calls a process can make. I can create a profile that allows only the specific syscalls Go needs to run the keyservice, and nothing else.
  • AppArmor (Application Armor) confines programs to a limited set of resources. I can define a policy that prevents the keyservice from writing to unexpected disk locations or accessing unauthorized network ports.

Together, these will create an even smaller attack surface, making a container breakout virtually impossible.

Deep Dive: The Backend (Go)

The main backend is the game's central nervous system, written in Go for its performance and concurrency. It's logically split into modules:

  • api: Defines all the REST endpoints for the frontend. It includes protection against slow header attacks to prevent resource exhaustion.
  • blockchain: Handles all interaction with the smart contract. It uses versioned auto-generated Go bindings from the contract's ABI. This is also where I used ERC1967Proxy to interact with the upgradeable proxy contract, allowing the backend to seamlessly call functions on the implementation contract through the stable proxy address.
  • listener: On startup, it quickly reads past blockchain events to catch up to the current state, then switches to a slower, regular polling of new events.
  • game: The largest and most complex module, containing the entire game state machine and lifecycle.
  • ws: Manages the WebSocket connections. To join a game, the user signs a nonce (a single-use random string) provided by the backend. This proves ownership of their address without a full transaction and also registers any associated referrer. The backend verifies this signature and, upon success, issues a one-time WebSocket authentication token. The frontend then uses this token to establish a secure, authenticated connection, preventing unauthorized access.
  • store & models: Manages database interaction using GORM, which provides a fantastic object-relational mapping layer and handles database schema migrations automatically. This is also where the analytics models for the conversion funnel and profit reports live.

Testing and Quality: I was relentless with testing.

  • Most modules have 100% test coverage, verified with go test -coverprofile.
  • I used golangci-lint with a suite of static analyzers like gosec (security), staticcheck, and govet to catch potential issues early.
  • Database tests were not mocked. I used the testcontainers pattern, where a real PostgreSQL Docker container is spun up for the test suite and torn down afterward, ensuring tests run against a real environment.
  • I heavily profiled the code for CPU usage, memory allocations (-cpuprofile, -memprofile), and lock contentions (-blockprofile, -race) to hunt down performance bottlenecks and race conditions.
  • Critical modules were compiled with garble to obfuscate the code, and all binaries were packed with upx --best --lzma to shrink their size and make reverse-engineering a nightmare.
  • Finally, the entire codebase was analyzed with SonarQube to enforce best practices and catch any remaining code smells.

Deep Dive: The Frontend (HTML/CSS/JS)

The frontend is intentionally simple: vanilla HTML, CSS, and JavaScript (transpiled from TypeScript). This wasn't a shortcut; it was a strategic choice. A simple, static site can be easily hosted on decentralized platforms like IPFS or Arweave, further enhancing the project's censorship resistance.

Even with its simplicity, it's well-tested using Jest for unit tests (app.test.js), ESLint for code quality, and Prettier for consistent formatting.

The Community and The Road Ahead

A project is nothing without a community. My growth strategy is focused on rewarding early believers.

  • Zealy Campaign: I launched a campaign on Zealy where users can complete quests to earn XP.
  • NFT Airdrop: The first 300 community members will receive a special NFT, granting them the "OG Member" role in Discord and future in-game bonuses.
  • Future Fun: I'm planning to add a global leaderboard and host tournaments with real prize pools.

This project has been an incredible journey. It started as a simple idea and, with the help of my AI partner, evolved into a secure, robust, and fully decentralized application. I went from a junior-level coder to a full-stack dApp creator, and I did it by focusing on the vision and letting the AI handle the complex implementation.

This is the new frontier of indie development. If you have an idea, the tools to build it are more accessible than ever.


Come play a game and join the community!

  • Play the game: muschairs.com
  • Official Repository: github.com/crow-004/musical-chairs-game
  • Join our Discord: discord.gg/wnnJKjgfZW
  • Follow on X/Twitter: @crow004_crow
  • Follow on Damus: npub1v0kc8fwz67k0mv539z6kaw5h25et9e2zmnnqq6z2naytaq566gwqkzz542

My next steps are to spread the word on platforms like Reddit, connect with web3 enthusiasts, and, of course, start building my next idea. Thanks for reading!

Top comments (0)