Skip to content

fix: secure activity rewards against forged sessions and coin farming - #14

Open
Xaxxoo wants to merge 1 commit into
DogStark:mainfrom
Xaxxoo:fix/issue-9-secure-activity-rewards
Open

fix: secure activity rewards against forged sessions and coin farming#14
Xaxxoo wants to merge 1 commit into
DogStark:mainfrom
Xaxxoo:fix/issue-9-secure-activity-rewards

Conversation

@Xaxxoo

@Xaxxoo Xaxxoo commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Fixes #9

The existing recordActivity() accepted arbitrary scores from any registered wallet and awarded DAILY_REWARD_COINS on every call — an attacker could farm 100 coins in minutes and drain the CELO reward pool. This PR adds five layers of defense:

1. Daily reward cap

The base 10-coin reward is awarded at most once per player per day via a dailyRewardClaimed[player][day] mapping. Perfect-score bonus (20 coins) is still awarded per qualifying session. This directly prevents same-day coin farming.

2. EIP-712 session attestation

When the owner sets a sessionSigner, every recordActivity call must carry a valid EIP-712 signature covering (player, score, correct, attempts, topic, nonce, deadline). The following attack vectors are tested and revert:

  • Forged signatures (wrong signer)
  • Expired attestations (deadline < block.timestamp)
  • Replayed signatures (nonce already consumed)
  • Cross-wallet attestations (signature bound to a different player)

When sessionSigner == address(0), no signature is required — this provides a migration path for the existing deployment.

3. Input bounds

  • score <= 1000 per session
  • attempts <= 100 per session
  • correct <= attempts
  • topic.length <= 32 bytes

4. Rate limiting

Max 10 sessions per player per day via dailySessionCount mapping.

5. Emergency pause

Owner can pause() / unpause() all activity recording, registration, and CELO claims using OpenZeppelin's Pausable. This provides an operational response mechanism for the deployed contract.

Other changes

  • hardhat.config.ts: Cancun EVM target + viaIR for OpenZeppelin v5.6 compatibility
  • lib/useContract.ts: passes deadline=0 and empty signature (no-op when signer not configured)
  • contracts/scripts/daily-activity.ts: updated keeper call to new ABI

Test coverage (35 tests, all passing)

Category Tests
Daily reward cap Awards once/day, resets next day, perfect bonus still works, farming yields only 10 coins
Input bounds Score > 1000, attempts > 100, topic > 32 bytes all revert
Rate limiting 11th same-day session reverts
EIP-712 attestation Valid sig accepted; wrong signer / expired / replayed / cross-wallet all revert; nonce increments
Emergency pause Registration, activity, claims all blocked; owner-only
Session signer mgmt Owner set/disable, non-owner reverts
CELO rewards Cannot farm to threshold in one day

Migration notes

The new contract has a different ABI for recordActivity (adds deadline and signature parameters). A new deployment is required. The migration path:

  1. Deploy the new contract
  2. Fund the new reward pool
  3. Leave sessionSigner as address(0) initially (existing behavior)
  4. Set up a backend attestation service
  5. Call setSessionSigner(backendAddress) to enable signature verification
  6. Update frontend to fetch signed attestations from the backend before submitting

Test plan

  • npx hardhat test — all 35 tests pass
  • Same-day farming yields only 10 coins (not 100+)
  • Forged/expired/replayed/cross-wallet attestations revert
  • 11th same-day session reverts
  • Pause blocks all state-changing operations
  • Score/attempts/topic bounds enforced

🤖 Generated with Claude Code

Threat model: any registered address could call recordActivity() with
arbitrary scores and collect DAILY_REWARD_COINS on every call, draining
the reward pool.  No session verification existed.

Contract changes (MathBlocGame.sol):
- Daily reward cap: base 10-coin reward is awarded at most once per
  player per day via dailyRewardClaimed mapping.  Perfect-score bonus
  (20 coins) is still awarded per qualifying session.
- EIP-712 session attestation: when owner sets a sessionSigner, every
  recordActivity call must carry a valid EIP-712 signature covering
  (player, score, correct, attempts, topic, nonce, deadline).  Forged,
  expired, replayed, and cross-wallet attestations revert.  When
  sessionSigner == address(0), signatures are not required (migration
  path for the existing deployment).
- Per-player nonces prevent replay attacks.
- Input bounds: score <= 1000, attempts <= 100, topic <= 32 bytes.
- Rate limit: max 10 sessions per player per day.
- Emergency pause: owner can pause/unpause all activity recording,
  registration, and CELO claims via OpenZeppelin Pausable.
- Leaderboard empty-array fix (topN=0 or 0 players returns []).

Infrastructure:
- hardhat.config.ts: enable Cancun EVM target + viaIR for OZ v5.6
  compatibility.
- lib/useContract.ts: pass deadline=0 and empty signature to the new
  recordActivity ABI (no-op when signer is not configured).
- contracts/scripts/daily-activity.ts: updated keeper call signature.

Tests (35 passing):
- Daily reward cap: single-day farming yields only 10 coins.
- Input bounds: score > 1000, attempts > 100, topic > 32 bytes revert.
- Rate limit: 11th same-day session reverts.
- EIP-712: valid signature accepted; wrong signer, expired, replayed,
  and cross-wallet attestations revert; nonce increments correctly.
- Emergency pause: registration, activity, and claims blocked.
- Session signer management: owner-only set/disable.

Closes DogStark#9

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Secure activity rewards against forged sessions and same-day coin farming

1 participant