Skip to content

Latest commit

ย 

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Riguwa โ€” a hand-drawn capital R in heavy black outline with hatched shading

Riguwa

A ballpoint-doodle survival shooter where the reward is real โ€” and the chain proves you earned it before it pays.

A staked arena on Creditcoin Testnet (102031). Stake tCTC or USDT, survive the waves, get paid by how far you got. Or pay your entry on Ethereum Sepolia and play without ever holding tCTC โ€” the Attestcoin Protocol proves your payment on-chain, with no oracle in between.


Play Network Contracts Tests Track

โ–ถ Play it โ€” riguwa.xyz ยท โ–ถ Demo video ยท Attestcoin Integration ยท Deployment ยท Explorer ยท Attestcoin Docs


In-game: a first-person view of a blue ballpoint city drawn on ruled notebook paper, rifle held at the bottom right, HUD showing SCORE 0, WAVE 1, 7 enemies left and 120 HP

Wave 1 of a staked run. No models, no textures, no sound files: the paper, the ink outlines, the hatching, the enemies and the 8-bit music are all generated in code. The seed for this run came from blockhash before a single enemy spawned.


The problem

Web3 games hand out tokens for things nobody can check. A leaderboard is a database row. A reward is an airdrop from a spreadsheet. When a game does put value on-chain, it almost always trusts a server to say who won โ€” and asks you to trust the server too.

The value is on-chain. The reason you earned it is not.

And when a game reaches across chains, it usually reaches through a bridge or an oracle operator you have to trust with the answer.


What Riguwa does

The game is a finished first-person survival shooter, drawn in blue ballpoint on lined notebook paper and rendered entirely in code โ€” no models, no textures, no sound files. Every outline, every enemy, every 8-bit music track is procedural.

On top of it sits a staking layer where the chain settles the outcome, and a cross-chain entry path where the protocol itself verifies your payment.

๐ŸŽฎ Stake to play Stake tCTC or USDT on a run. Reach wave 5 for 1.5x, wave 10 for 2x, wave 15 for 3x. Below wave 5 the stake joins the pool. There is no free play.
๐Ÿ”— Cross-chain entry, gasless Pay on Sepolia; the Attestcoin block-prover precompile verifies that transaction inside the same Creditcoin transaction that credits you. You never hold tCTC.
๐ŸŽฒ The chain seeds the run startRun issues a seed from blockhash. The game's PRNG takes it, so the wave you faced was committed before you played.
๐Ÿ” Solvency by construction startRun reserves the payout ceiling before accepting a stake. The contract can never owe more than it holds โ€” asserted as a fuzz invariant.

๐ŸŸข Live on Creditcoin Testnet โ€” verify it yourself

Every contract is source-verified on Blockscout. Every claim below has a link.

Core contracts (UUPS ยท ERC-7201 namespaced storage)

Contract Address
ArenaEscrow โ€” stakes, pool, settlement 0xD63CbB36โ€ฆ19f92Ee7
DoodleGateASC โ€” the Attestcoin Smart Contract 0xd6565056โ€ฆ248f09dF
SeasonRegistry โ€” per-season leaderboard 0xc588f37dโ€ฆ5C732050
USDT โ€” mock test stablecoin, 6 dp 0x47dcAB80โ€ฆ3cbFA5B1

Blockscout showing ArenaEscrow with a green "Contract source code verified (exact match)" banner, detected as an EIP-1967 proxy, compiled with solc 0.8.30 targeting the cancun EVM

ArenaEscrow on Blockscout. Verified exact-match, detected as an EIP-1967 proxy โ€” the upgradeable requirement, met and checkable. All four proxies needed --skip-is-verified-check, because ERC1967Proxy runtime bytecode is identical across deployments and forge reports three of them as already verified.

Source chain (Ethereum Sepolia)

Contract Address
DoodleGate โ€” emits the two events, nothing else 0x56CeD9fDโ€ฆ6da76484

Receipts, not screenshots

Claim Proof
๐ŸŒ‰ Cross-chain entry works Sepolia payEntry โ†’ Creditcoin relay. 0.0005 ETH in, 0.05 USDT credited.
๐Ÿ” The precompile did the verifying That relay transaction's first log is TransactionVerified, emitted by 0xโ€ฆ0FD2 itself โ€” the block prover, not us.
๐Ÿ† A staked run paid out settleRun โ€” 1 tCTC staked, wave 12 reached, 2 tCTC paid.
โšก The protocol is live right now ./contracts/script/check-live.sh โ€” chain id, supported source chains, current attestation height.
โœ… The contracts pass forge test โ€” 139 passing, 95.19% lines, 98.04% functions.

Blockscout log view of the Creditcoin relay transaction: the first log is TransactionVerified, emitted by the BlockProver precompile, with chainKey 1 and Sepolia height 11695759

The relay transaction's first log. The emitter is BlockProver โ€” the precompile at 0xโ€ฆ0FD2, not our contract โ€” carrying chainKey 1 and the Sepolia height it verified. That is the whole point of the integration: we do not attest anything. The chain does, and we refuse to act until it has. The link above is there so you never have to trust this image.


How it works

You stake. You play. The chain decides what you earned.

flowchart LR
    A["๐Ÿ’ฐ <b>Stake</b><br/>startRun reserves the<br/>payout ceiling up front"]
    B["๐ŸŽฒ <b>Seed issued</b><br/>from blockhash, before<br/>you play a single wave"]
    C["๐ŸŽฎ <b>Survive</b><br/>ink-monitor watches live<br/>and checks the impossible"]
    D["๐Ÿ” <b>Attested</b><br/>EIP-712 RunResult signed<br/>and submitted for you"]
    E["๐Ÿ† <b>Paid</b><br/>wave 12 โ†’ 2x<br/>settled on-chain"]

    A --> B --> C --> D --> E

    style A fill:#0d2b4e,stroke:#1f6feb,color:#f0f6fc
    style B fill:#1c2128,stroke:#6e7681,color:#f0f6fc
    style C fill:#3d1f1f,stroke:#f85149,color:#f0f6fc
    style D fill:#0d2b4e,stroke:#1f6feb,color:#f0f6fc
    style E fill:#0f2d1c,stroke:#2ea043,color:#f0f6fc
Loading

And the cross-chain path, call by call โ€” including the two checks the ASC cannot skip:

sequenceDiagram
    autonumber
    actor U as ๐Ÿ‘ค Player
    participant SEP as โ›“๏ธ DoodleGate<br/>(Sepolia)
    participant REL as ๐Ÿšš ink-monitor<br/>relayer
    participant PRE as ๐Ÿ” BlockProver<br/>precompile 0xโ€ฆ0FD2
    participant ASC as ๐Ÿ›ก๏ธ DoodleGateASC<br/>(Creditcoin)
    participant ESC as ๐Ÿฆ ArenaEscrow

    rect rgba(219, 171, 10, 0.14)
    Note over U,SEP: Pay on Ethereum
    U->>SEP: payEntry(runRef) โ€” from your own wallet
    SEP-->>REL: ArenaEntryPaid(player, runRef, amount)
    end

    rect rgba(128, 142, 158, 0.12)
    Note over REL,PRE: Wait for the protocol, not for us
    REL->>PRE: waitUntilHeightAttested(chainKey 1, height)
    PRE-->>REL: attested
    REL->>REL: fetch Merkle + continuity proofs
    end

    rect rgba(31, 111, 235, 0.14)
    Note over REL,ESC: One Creditcoin transaction does all of it
    REL->>ASC: execute(action, chainKey, height, txBytes, proofsโ€ฆ)
    ASC->>ASC: dedupe by queryId
    ASC->>PRE: verifyAndEmit(...)
    PRE-->>ASC: โœ… TransactionVerified
    ASC->>ASC: require receiptStatus == 1
    ASC->>ASC: require emitter == sourceGate[chainKey]
    ASC->>ESC: mint entry credit
    end

    rect rgba(46, 160, 67, 0.14)
    Note over U,ESC: Play without ever holding tCTC
    U->>ESC: startRun(USDT, amount)
    ESC-->>U: runId + seed
    end
Loading

๐Ÿ” The two checks that carry the bridge

Both are called out in the Attestcoin documentation. Both have a dedicated negative test.

The precompile proves inclusion. It does not prove success.

A transaction that reverted on Sepolia is still in a real block on the real chain. Without require(receipt.receiptStatus == 1), a reverted entry payment would mint credit anyway.

Test: test_revert_transactionThatFailedOnTheSourceChain

Event signatures are public. The emitter is the secret.

if (_s().sourceGate[chainKey] != emitter || emitter == address(0)) {
    revert UnknownEmitter(chainKey, emitter);
}

Without this binding, anyone could deploy their own contract on Sepolia emitting a byte-identical ArenaEntryPaid and mint themselves unlimited credit. This is the single most important line in the repository.

Test: test_revert_eventFromAnUnregisteredEmitter

Replay is closed by queryId = keccak256(chainKey, blockHeight, txIndex), recorded in namespaced storage. Test: test_revert_replayOfTheSameQuery.


๐Ÿฆ Solvency is structural, not a policy

startRun reserves the payout ceiling from the pool before it accepts your stake, and refuses the run if the pool cannot cover it:

require(activeRun[msg.sender] == bytes32(0));       // one run at a time
require(amount <= maxStake[token]);                 // 10 tCTC
ceiling = amount * maxMultiplierBps / 10_000;       // 3x
require(pool.free >= ceiling);                      // โ† refuse rather than over-promise

Which makes this identity hold at every moment, and it is asserted as a fuzz invariant across 4,096 randomised calls:

balance(token) == pool.free + pool.reserved + pool.activeStake

A monitor outage cannot trap a stake. abandonRun is callable by anyone once the settlement window closes, so a player is always made whole without needing our cooperation.


๐Ÿค– ink-monitor โ€” one process, two jobs

The run monitor

A WebSocket accepts a socket only for a runId that is Active on-chain and owned by the claimed player. It then refuses anything the game's own wave formula says is impossible: a skipped wave, an impossibly fast clear, more kills than the wave could spawn, a death claimed above the wave reached.

On why it bounds rather than replays. An earlier design replayed the game's seeded RNG stream to derive exact wave composition. That was abandoned deliberately: startWave draws from the same stream for the modifier, every enemy, the flavour message, seven pickup positions and a jitter per pickup. Any change to the game shifts the stream, and the monitor would start rejecting honest runs. It computes an upper bound instead, which needs no RNG at all โ€” and a ceiling is exactly what a plausibility check needs.

When the run ends it signs an EIP-712 RunResult and submits it. The player already signed to stake; asking them to sign again to receive their payout is a poor trade.

The Attestcoin relayer

Watches DoodleGate on Sepolia, waits for Creditcoin's attestors to cover the block, fetches Merkle and continuity proofs from the Proof Builder, and submits them. The ASC verifies the proof itself โ€” the relayer is a courier, not an oracle. That is what makes cross-chain entry gasless.


โฑ๏ธ Try it yourself

1 Open the game Connect a wallet. It adds and switches to Creditcoin Testnet for you.
2 Get some USDT One click on the faucet โ€” 1,000 test USDT, rate limited per address.
3 Stake a run Pick tCTC or USDT, up to 10. The panel shows your balance, what the pool holds, and the stake it can actually back.
4 Survive Waves, bosses every fifth. The monitor watches live and the chain already knows your seed.
5 Get paid Die, and the payout lands before you leave the screen โ€” with a Blockscout link. No second signature.

The Reown AppKit modal open over the blurred game menu, listing WalletConnect with a QR code option, Trust Wallet, MetaMask, Binance Wallet, SafePal and a search for 70+ more

The staking panel: wallet address, tCTC and USDT balances, a token toggle, a stake amount capped at 10 tCTC, a line reading pool holds 1995 tCTC and backs a stake up to 665, and a PLAY STAKED RUN button

Steps 1 and 3. Reown AppKit on top of @wagmi/core, pre-bundled by hand into vendor/ because the game has no build step and is not getting one. The stake panel shows both balances and, more usefully, what the pool can actually back right now: the cap is 10 tCTC, but the number that matters is pool.free / 3, because startRun reserves the full 3x payout ceiling before it takes your money.

Or skip step 2 entirely: pay on Sepolia and let the precompile prove it.


๐Ÿ“ฆ What is in here

Path What's inside Verify it
contracts/ Four UUPS contracts on Creditcoin, one plain contract on Sepolia. Escrow, ASC, season registry, mock stablecoin. forge test โ€” 139 passing, 95.19% lines
server/ ink-monitor. Run monitor over WebSocket, Attestcoin relayer, EIP-712 signer. npm test โ€” 35 passing
game/doodleshooter/ The game. Vanilla ES modules, no build step, three.js and Reown AppKit vendored. node --test โ€” 12 passing
docs/ Design, implementation plans, and the Attestcoin integration document. Integration

โš–๏ธ Live parameters

Read them off the chain rather than taking our word:

Parameter Value Read it from
Max stake 10 tCTC / 100 USDT ArenaEscrow.maxStakeOf(token)
Payout tiers wave 5 โ†’ 1.5x ยท 10 โ†’ 2x ยท 15+ โ†’ 3x ArenaEscrow.multiplierBpsFor(wave)
Reward pool 2,000 tCTC + 100,000 USDT ArenaEscrow.poolOf(token)
Settlement window 2 hours, then abandonRun returns the stake ArenaEscrow.runTtl()
Attestor threshold 1 (Phase 1) ArenaEscrow.threshold()
Source chain Ethereum Sepolia, chainKey 1 (not 11155111) DoodleGateASC.sourceGateOf(1)
cast call 0xD63CbB36D1d25f44c653Ac5c6990B6B219f92Ee7 "multiplierBpsFor(uint32)(uint32)" 15 \
  --rpc-url https://rpc.cc3-testnet.creditcoin.network      # 30000 = 3x

๐Ÿ› ๏ธ Tech stack

Everything runs against Creditcoin Testnet 102031 (https://rpc.cc3-testnet.creditcoin.network).

โ›“๏ธ contracts/ โ€” Solidity

Layer What we use
Language Solidity 0.8.30
Toolchain Foundry โ€” forge ยท cast. Optimizer on, runs = 200, via_ir (several contracts hit stack-too-deep without it)
EVM target cancun, not shanghai โ€” OZ 5.7's Math.sol pulls in Bytes.sol, which emits MCOPY. Creditcoin was probed directly: PUSH0, MCOPY and TSTORE/TLOAD all execute; BLOBBASEFEE is rejected
Libraries OpenZeppelin 5.7 + Contracts-Upgradeable โ€” UUPS, ERC-7201 namespaced storage, EIP712, ECDSA, SafeERC20
Attestcoin @gluwa/asc-contracts 0.2.1 โ€” EvmV1Decoder, INativeQueryVerifier
Tests 139 passing โ€” positive, edge and negative cases, fuzz, invariants, UUPS upgrade safety, and a mocked block-prover etched at 0xโ€ฆ0FD2

โš™๏ธ server/ โ€” ink-monitor

Layer What we use
Runtime Node 24, TypeScript run directly via --experimental-strip-types โ€” no build step
Chain viem 2 โ€” clients, contract reads and writes, EIP-712 signing, Sepolia event watching
Attestcoin @gluwa/usc-sdk 0.18 โ€” PrecompileChainInfoProvider, ProofBuilder. ethers is scoped to this one file, because the official SDK takes an ethers provider
Transport ws 8 โ€” the run-monitor WebSocket
Tests 35 passing with node --test, including a PRNG parity vector generated by the game's own implementation

๐ŸŽฎ game/doodleshooter/ โ€” the game

The title screen: a ballpoint-blue menu panel over the hand-drawn city, with the control list for mouse and keyboard and for a PS5 controller

The menu is plain DOM drawn in the same pen style over the canvas, and every size in it is clamp()ed against viewport height so the panel scales instead of spilling off the page.

Layer What we use
Rendering three.js, vendored. The scene renders to a buffer of shade, ink id and view-space normals plus depth; a post pass draws outlines from an inverse-depth Laplacian, then hatching, paper grain, ruled lines and the red margin
Audio Procedural WebAudio โ€” effects and a per-map 8-bit score, no sound files
Web3 Reown AppKit 1.8 + @wagmi/core + viem 2. wagmi proper needs React; the game is vanilla, so it runs on the framework-agnostic core the adapter builds on anyway
Build None. AppKit, wagmi and viem are pre-bundled once by tools/bundle-appkit.sh into a single 1.2 MB (gzipped) ES module and committed to vendor/, exactly like three.js. The game installs nothing and builds nothing
Tests 12 passing โ€” an English-only guard over every source file, and PRNG determinism

๐Ÿ”’ Security & limitations

We would rather you read this than discover it.

  • Phase 1 trusts the monitor for the score. Stated plainly rather than hidden. It does not touch the cross-chain claim: every piece of cross-chain data is proven by the precompile with no oracle. The attestor key holds no owner rights โ€” it can sign a RunResult and nothing else โ€” and is rotatable with two calls.
  • Phase 2 is a configuration change, not a rewrite. settleRun verifies N-of-M attestations. Phase 1 registers one attestor with threshold 1; Phase 2 registers match participants with threshold ceil(2n/3). A test proves the switch needs no contract change.
  • Online multiplayer is deliberately gated off. The peer-to-peer code is intact but is Phase 2.
  • Attestcoin Writability is not released on testnet, so ETH paid on Sepolia stays in DoodleGate and rewards pay in tCTC or USDT. DoodleGate.withdraw marks where two-way settlement lands when Writability ships.
  • Attestation lag is 20โ€“40 minutes on CC3 testnet โ€” measured, not assumed. Cross-chain entry is not instant, and the relayer waits an hour before giving up.
  • The USDT contract is a testnet-only mock deployed by this project. It is not issued by, affiliated with, or endorsed by Tether, and holds no value.

๐Ÿงช Tests

cd contracts          && forge test              # 139 passing ยท 95.19% lines ยท 98.04% functions
cd server             && npm test                # 35 passing
cd game/doodleshooter && node --test             # 12 passing

Plus the checks that only mean something against the real chain:

./contracts/script/check-live.sh                 # protocol liveness, opcode probe
cd server && npm run check:attestcoin            # drive the SDK against the live precompile

forge test --fork-url does not work against Creditcoin: its blocks carry no mixHash and report difficulty: 0x0, so Foundry's fork backend rejects them with header validation error: prevrandao not set. check-live.sh makes the same assertions over plain JSON-RPC and works today.



Built for BUIDL CTC 2026 Fall ยท Gaming track

Attestcoin Integration ยท Deployment ยท Explorer ยท Attestcoin Docs

Testnet only. USDT here is a project-issued mock with no value and no affiliation with Tether.

About

A ballpoint-doodle survival shooter with staked runs on Creditcoin. Cross-chain entry verified by the Attestcoin BlockProver precompile, not by a bridge or an oracle.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages