game-wager-v1
๐ game-wager-v1
P2P Game Wagering Contract โ Independent Security Audit
Summary
| Severity | Count |
|---|---|
| CRITICAL | 0 |
| HIGH | 1 |
| MEDIUM | 2 |
| LOW | 2 |
| INFO | 2 |
This is a well-structured P2P wagering contract using Clarity 4's as-contract? with explicit with-ft allowances. Signature authentication follows SIP-018 structured data with proper domain separation via auth-v7. The main risks are centralization around the oracle role and fee extraction via cancellation.
Architecture Overview
The contract implements a custodial P2P wagering system:
- Deposit: Users transfer SIP-010 tokens into the contract, credited to their secp256k1 pubkey
- Create Game: Oracle submits both players' signed wager commitments, debiting their balances
- Resolve: Oracle declares the winner, who receives the pot minus fees
- Withdraw: Users withdraw to their registered wallet (or a specified recipient) via signed request
- Cancel: Oracle or anyone (after timeout) can cancel, refunding both players minus fees
Positive Findings
- โ
Uses Clarity 4
as-contract?with explicitwith-ftallowances โ eliminates blanket asset access - โ SIP-018 structured data hashing with chain-id and contract address in domain โ proper cross-chain/cross-contract replay protection
- โ
Signature nonce tracking via
used-signaturesmap prevents replay attacks - โ Token whitelist prevents malicious token contract interactions
- โ Fee rates capped (wager: 20%, withdraw: 10%)
- โ GAME_TIMEOUT safety valve (144 blocks โ 24h) prevents permanent fund lockup
- โ Same-player check prevents self-wagering
Findings
HIGH H-01: Oracle has unilateral control over game outcomes
Location: resolve-game, cancel-game
Description: The oracle principal has unchecked power to resolve any game in favor of either player. There is no dispute mechanism, no timelock on resolution, no multi-oracle quorum, and no on-chain game state verification. A compromised or malicious oracle can systematically resolve games in favor of a colluding player.
(define-public (resolve-game (game-id uint) (winner (buff 33)))
(let (...)
(asserts! (is-eq tx-sender (var-get oracle)) err-not-oracle)
;; Oracle picks any player as winner โ no verification of actual game outcome
(asserts!
(or (is-eq winner (get player-a game))
(is-eq winner (get player-b game)))
err-invalid-winner)
(credit-balance winner token payout)
...))
Impact: Total loss of wagered funds for all users if oracle is compromised. The oracle can also cancel-game at any time (no timeout required for oracle), extracting fees from both players.
Recommendation:
- Implement a multi-oracle or threshold signature scheme for game resolution
- Add a dispute window after resolution where players can challenge (with bond)
- Consider committing game results on-chain (e.g., hash of game state) before resolution
- At minimum, add a timelock on resolution to allow monitoring
MEDIUM M-01: Oracle cancel griefing extracts fees from players
Location: cancel-game
Description: The oracle can cancel any active game without waiting for the timeout. Cancellation charges withdraw-fee-rate to both players. A malicious oracle could let users create games, then cancel them instead of resolving, farming fees.
(define-public (cancel-game (game-id uint))
(let (...)
(asserts!
(or (is-eq tx-sender (var-get oracle)) ;; Oracle can cancel immediately
(> burn-block-height (+ (get created-at game) GAME_TIMEOUT)))
err-game-not-expired)
;; Both players lose fee
(credit-balance (get player-a game) tokn refund) ;; refund = wager - fee
...))
Impact: At current withdraw-fee-rate of 1% (u100), the oracle could extract 1% of every wager by cancelling instead of resolving. With maximum fee rate (10%), this becomes significant.
Recommendation: Oracle-initiated cancellations should refund the full wager (no fee), or require a minimum number of blocks before oracle can cancel.
MEDIUM M-02: Deployer can rug via fee rate + treasury changes
Location: set-fee-rate, set-withdraw-fee-rate, set-treasury
Description: The deployer can change fee rates up to 20% (wager) and 10% (withdraw) at any time, and redirect the treasury to any address. These changes apply immediately to all future game resolutions and withdrawals, with no timelock or user notification.
(define-public (set-fee-rate (new-fee-rate uint))
(begin
(asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
(asserts! (<= new-fee-rate u2000) err-invalid-amount) ;; Up to 20%
(var-set fee-rate new-fee-rate)
(ok true)))
Impact: Users who deposited under one fee regime may have their funds subject to significantly higher fees. Combined with treasury redirect, this is a rug vector.
Recommendation:
- Add timelock (e.g., 144 blocks) before fee changes take effect
- Emit events for all admin changes (see L-02)
- Consider snapshot-based fees: lock fee rate at game creation time
LOW L-01: No draw resolution mechanism
Location: resolve-game
Description: Games can only be resolved with a winner or cancelled. There is no draw mechanism that returns full wagers without charging fees. If a game ends in a draw, the only option is cancel-game which charges withdraw-fee-rate to both players.
Impact: Players lose funds to fees on legitimate draws. For games with frequent draws, this creates an unfair cost.
Recommendation: Add a draw-game function that returns full wagers with no fee, callable by oracle only.
LOW L-02: Admin functions lack event emission
Location: set-oracle, set-fee-rate, set-withdraw-fee-rate, set-treasury
Description: All admin configuration changes execute silently with no print events. This makes it difficult for users and monitoring systems to detect changes to critical parameters like oracle address, fee rates, and treasury.
Impact: Reduced transparency. Fee changes or oracle swaps could go unnoticed by users.
Recommendation: Add (print { event: "oracle-updated", ... }) to all admin functions, consistent with existing event patterns in deposit, withdraw, etc.
INFO I-01: Double fee on winning โ wager fee + withdrawal fee
Location: resolve-game, withdraw
Description: When a game is resolved, the winner's payout already has the wager fee deducted (up to 5% of pot). When the winner later withdraws, they pay an additional withdrawal fee (up to 1%). This results in a cumulative fee that may not be obvious to users.
Impact: At default rates: 5% wager fee + 1% withdrawal fee = ~5.95% effective fee. At maximum rates: 20% + 10% = ~28% effective fee.
Recommendation: Clearly document the fee structure for users. Consider waiving withdrawal fees on game payouts.
INFO I-02: Deposit to arbitrary pubkey without ownership proof
Location: deposit
Description: Anyone can deposit tokens to any pubkey without proving ownership of that pubkey. While likely by design (allowing deposits on behalf of others), this means an attacker could create many small deposits to arbitrary pubkeys, polluting the state.
Impact: Minimal โ state pollution only. The funds are still accessible by the pubkey owner.
Recommendation: Consider minimum deposit amounts or requiring signature auth for deposits (similar to the build-wager-deposit-hash that exists in auth-v7 but is unused in game-wager-v1).
Documented Limitations
- The system is inherently custodial โ all token balances are held by the contract
- Oracle trust is a fundamental design assumption โ users must trust the game server to report honest outcomes
- auth-v7 is hardcoded to this specific contract deployment โ not upgradeable
auth-v7 Review
The companion contract SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK.auth-v7 was reviewed for signature security:
- โ
Uses SIP-018 structured data prefix (
0x534950303138) - โ
Domain hash includes
chain-idand the specific contract principal โ prevents cross-chain and cross-contract replay - โ
Each message type includes a
topicfield โ prevents cross-function replay - โ
auth-idin every message provides a nonce mechanism (combined withused-signaturesmap in game-wager-v1) - โน๏ธ Note: auth-v7 has an unused
build-wager-deposit-hashfunction, suggesting a planned signed-deposit feature not yet implemented