A privacy-preserving "Lowest Unique Number" game powered by Inco's Fully Homomorphic Encryption (FHE) on Base Sepolia.
- The Problem
- Our Solution
- Why Inco FHE?
- How It Works
- Technical Architecture
- Security Features
- Smart Contract
- Getting Started
- Future Improvements
- Limitations & Challenges
Traditional blockchain games face a fundamental transparency problem: everything is public.
Consider a simple number-guessing game where the lowest unique number wins. On a standard blockchain:
❌ Player A submits "3" → Everyone sees it
❌ Player B sees "3" is taken, submits "2" → Guaranteed win
❌ The game becomes about timing, not strategy
This breaks the core mechanic. Players who submit later have an unfair advantage because they can see all previous choices. The game devolves into a race condition rather than a strategic exercise.
Existing "solutions" have significant drawbacks:
| Approach | Problem |
|---|---|
| Commit-Reveal | Requires two transactions, players can abandon after seeing commits |
| Trusted Server | Centralization, single point of failure |
| Zero-Knowledge Proofs | Complex, expensive, limited composability |
Cognumbers uses Inco's Fully Homomorphic Encryption to create a truly fair game where:
✅ Player A submits encrypted "3" → No one can see it
✅ Player B submits encrypted "2" → No one can see it
✅ Game ends → Inco decrypts all choices simultaneously
✅ Winner determined fairly → "2" was the lowest unique number
The encryption happens on-chain, meaning:
- No trusted third party
- No commit-reveal complexity
- No timing advantages
- Pure strategy
FHE allows computation on encrypted data without decrypting it first. This is revolutionary for blockchain privacy:
Traditional Encryption:
Encrypt(A) + Encrypt(B) = ??? (meaningless)
Homomorphic Encryption:
Encrypt(A) + Encrypt(B) = Encrypt(A + B) ✓
Inco operates as a confidential computing coprocessor for EVM chains. Here's why we chose it:
// Encrypted types work like regular Solidity types
euint256 encryptedChoice = _encryptedChoice.newEuint256(msg.sender);
// Perform encrypted comparisons
ebool isMatch = encryptedChoice.eq(targetNumber);
// Conditional logic on encrypted data
euint256 result = isMatch.select(valueIfTrue, valueIfFalse);Unlike centralized solutions, Inco uses a network of covalidators who must collectively sign off on decryptions:
Player submits encrypted choice
↓
Stored on Base Sepolia (encrypted)
↓
Game ends → Decryption requested
↓
Inco covalidators verify & sign
↓
Attestation returned to contract
↓
Contract verifies signatures
↓
Winner determined trustlessly
Because Inco integrates at the EVM level, encrypted values can:
- Be stored in mappings
- Participate in contract logic
- Interact with other contracts
- Maintain privacy across calls
Players don't need to:
- Run special software
- Manage encryption keys
- Perform multi-round protocols
- Trust a central server
They simply submit their choice, and the SDK handles encryption client-side before the transaction.
┌─────────────────────────────────────────────────────────────────┐
│ GAME LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CREATE 2. JOIN 3. FINALIZE │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Creator │ │ Players │ │ Anyone │ │
│ │ sets: │ │ submit │ │ calls │ │
│ │ - Fee │ ───► │encrypted│ ───► │finalize │ │
│ │ - Time │ │ numbers │ │after │ │
│ └─────────┘ └─────────┘ │deadline │ │
│ └─────────┘ │
│ │ │
│ ▼ │
│ 5. PAYOUT 4. RESOLVE │
│ ┌─────────┐ ┌─────────┐ │
│ │ Winner │ │Inco │ │
│ │receives │ ◄─── │decrypts │ │
│ │ prize │ │& attests│ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Players choose a number from 1-10. The winner is whoever picked the lowest number that no one else picked.
Example:
| Player | Choice |
|---|---|
| Alice | 3 |
| Bob | 1 |
| Carol | 1 |
| Dave | 2 |
1is not unique (Bob and Carol both chose it)2is unique and lowest- Dave wins!
This creates interesting game theory:
- Picking
1seems optimal, but everyone thinks that - Picking a higher number is safer but less likely to win
- The optimal strategy depends on predicting others' behavior
Without encryption, this game is broken. Later players can simply pick an unused low number.
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ React + Vite + TypeScript │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ RainbowKit │ │ Wagmi │ │ Inco SDK │ │
│ │ Wallet │ │ Contract │ │ Encryption │ │
│ │ Connection │ │Interactions │ │ Client │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BASE SEPOLIA │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Cognumbers.sol │ │
│ │ - Game state management │ │
│ │ - Entry fee collection │ │
│ │ - Encrypted choice storage │ │
│ │ - Winner calculation │ │
│ │ - Prize distribution │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INCO COPROCESSOR │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ FHE │ │ Covalidator │ │ Attestation │ │
│ │ Runtime │ │ Network │ │ Service │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
// 1. User selects number (client-side)
const choice = 5;
// 2. Inco SDK encrypts for the contract
const encryptedChoice = await incoClient.encrypt(choice);
// 3. Submit to contract
await contract.joinGame(gameId, encryptedChoice, { value: entryFee });
// 4. Contract stores encrypted value
playerChoices[gameId][msg.sender] = encryptedChoice.newEuint256(msg.sender);// 1. Request decryption from Inco
const handles = players.map(p => contract.playerChoiceHandles(gameId, p));
const decryptionResult = await incoClient.decryptMultiple(handles);
// 2. Inco covalidators sign attestations
// (happens automatically via Inco network)
// 3. Submit attested decryptions to contract
await contract.resolveWinner(
gameId,
decryptionResult.values, // Decrypted numbers
decryptionResult.signatures // Covalidator signatures
);
// 4. Contract verifies attestations
for (uint256 i = 0; i < choices.length; i++) {
DecryptionAttestation memory attestation = DecryptionAttestation({
handle: playerChoiceHandles[gameId][players[i]],
value: bytes32(choices[i])
});
// Verify covalidator signatures
require(
inco.incoVerifier().isValidDecryptionAttestation(attestation, signatures[i]),
"Invalid attestation"
);
}Our contract implements industry-standard security practices:
contract Cognumbers is ReentrancyGuard {
function resolveWinner(...) external nonReentrant {
// State updated BEFORE external calls (CEI pattern)
game.status = GameStatus.Finished;
// External call last
(bool success, ) = winner.call{value: prize}("");
}
}contract Cognumbers is Ownable, Pausable {
function pause() external onlyOwner {
_pause();
}
function emergencyWithdraw(address _to, uint256 _amount)
external
onlyOwner
whenPaused
{
// Emergency recovery only when paused
}
}// Cannot submit fake decryption results
if (!inco.incoVerifier().isValidDecryptionAttestation(attestation, signatures[i])) {
revert InvalidAttestation(gameId, i);
}error GameNotOpen(uint256 gameId, GameStatus currentStatus);
error IncorrectEntryFee(uint256 gameId, uint256 required, uint256 provided);
error AlreadyJoined(uint256 gameId, address player);
// ... 15+ custom errors for precise debugging// If no unique number exists, players get refunds
if (winner == address(0)) {
game.status = GameStatus.Refunded;
emit RefundsInitiated(gameId, playerCount, prizePool);
}
// Players claim refunds individually (pull pattern)
function claimRefund(uint256 _gameId) external nonReentrant {
require(game.status == GameStatus.Refunded || game.status == GameStatus.Cancelled);
require(hasJoined[_gameId][msg.sender]);
require(!hasClaimedRefund[_gameId][msg.sender]);
hasClaimedRefund[_gameId][msg.sender] = true;
(bool success, ) = msg.sender.call{value: game.entryFee}("");
}Deployed Address: 0x3C20F0548933663cD13cCF2884a7bb785EF9766D
Network: Base Sepolia (Chain ID: 84532)
Package Versions:
@inco/js: 0.7.11@inco/lightning: 0.7.11
View on Basescan: Link
| Function | Description |
|---|---|
createGame(entryFee, duration) |
Create a new game with specified parameters |
joinGame(gameId, encryptedChoice) |
Join with an encrypted number (1-10) |
finalizeGame(gameId) |
Lock the game after deadline |
resolveWinner(gameId, choices, signatures) |
Submit attested decryptions |
claimRefund(gameId) |
Claim refund if game cancelled/no winner |
cancelGame(gameId) |
Cancel if deadline passed with <2 players |
| Parameter | Value |
|---|---|
| Min Players | 2 |
| Max Players | 10 |
| Number Range | 1-10 |
| Min Duration | 60 seconds |
| Max Duration | 7 days |
- Node.js 18+
- A wallet with Base Sepolia ETH (Faucet)
cd cognumbers-frontend
npm install
npm run devcd cognumbers-contracts
npm install # Install Inco & OpenZeppelin
forge build # Compile
forge test # Run tests# Set environment variables
cp .env.example .env
# Edit .env with your PRIVATE_KEY and RPC URL
# Deploy
forge script script/Cognumbers.s.sol:CognumbersScript \
--rpc-url $BASE_SEPOLIA_RPC_URL \
--broadcast- Event indexing with The Graph for game history
- Leaderboard tracking wins/losses per address
- Mobile-optimized UI improvements
- Gas optimization for bulk operations
- Tournament mode with brackets
- Variable number ranges (1-100, etc.)
- Team-based gameplay
- Integration with ENS for player names
- Cross-chain deployment (Arbitrum, Optimism)
- DAO governance for game parameters
- NFT rewards for winners
- Reputation system
-
Decryption Latency
- Inco decryption requires covalidator consensus
- Adds ~10-30 seconds to resolution
- Future improvements expected as network matures
-
Gas Costs
- FHE operations are more expensive than plaintext
- Mitigated by Base L2's low fees
- Encrypted counter updates add overhead
-
Player Cap
- Currently limited to 10 players per game
- Scaling requires optimizing encrypted aggregation
-
Attestation Verification
- Initially unclear how to verify Inco decryptions
- Solution: Use
inco.incoVerifier().isValidDecryptionAttestation()
-
Encrypted Arithmetic
- Counting unique numbers requires comparison loops
- Solution: Pre-compute encrypted counters per number
-
Refund Edge Cases
- What if no unique number exists?
- Solution: Implemented
Refundedstatus with pull-based claims
- Inco Network - FHE infrastructure
- Base - L2 deployment
- RainbowKit - Wallet connection
- Foundry - Smart contract tooling
MIT License - see LICENSE for details.
Built for the future of on-chain privacy
Cognumbers - Where your strategy stays secret until the very end.