# Duel Arena: build and review report

Deliverable: a standalone Foundry project implementing `DuelToken` and `DuelArena` for the
`evm_project` launch pipeline on Sepolia under policy v3, with tests, README and this report.

## What was delivered

| Path                              | Content                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| `src/DuelToken.sol`               | Fixed-supply ERC-20 "Duel Arena" / `DUEL`, 18 decimals, 10^27 minted to `msg.sender`, no constructor args, no admin paths |
| `src/DuelArena.sol`               | Commit-reveal rock-paper-scissors escrow, non-upgradeable, no admin, pull payments, liability tracking |
| `test/*.t.sol`                    | 8 suites, 128 tests: unit, boundary, adversarial token, fuzz, stateful invariants, deployment floor rehearsal |
| `lib/forge-std`                   | forge-std v1.9.7 vendored as plain files (no submodule)                              |
| `foundry.toml`, `remappings.txt`  | solc 0.8.26, cancun, optimizer 200, `bytecode_hash = "none"`, `ffi = false`, `fs_permissions = []` |
| `README.md`                       | Architecture, exact ABIs, state/outcome tables, lock-up trade-off, token acquisition, cast walkthrough, deployment parameters, limitations |

Not delivered on purpose: `launch.json` (owned by the separate manifest assignment), any
frontend, badges, NFTs or extra game modes, and any on-chain transaction. No wallet key was read
and nothing was broadcast.

## Design decisions

- **Identifiers.** `duelId = keccak256(abi.encode(creator, nonce))`. `createDuel` takes the
  expected nonce, compares it with `nonces[creator]`, and increments in the same call. A racing
  second transaction reverts with `NonceMismatch`; ids are never reused because nonces only grow.
- **Commitment.** `keccak256(abi.encode(block.chainid, address(this), duelId, player, uint8(move), bytes32(salt)))`.
  Reveal recomputes it with `msg.sender` as the player, so a copied commitment can never be
  revealed by the copier. The move is `uint8` in the external API and validated to `<= 2` with a
  custom error rather than an ABI panic.
- **Deadlines.** Join and reveal require `block.timestamp < deadline`; expiry and timeout require
  `>=`. `joinDeadline = createdAt + joinWindow`, `revealDeadline = joinedAt + revealWindow`.
  Windows are validated in the constructor to `(0, 365 days]` so `uint64` deadline arithmetic
  cannot overflow.
- **Stake bound.** `MAX_STAKE = type(uint128).max`; `2 * stake` and `totalLiability` therefore
  cannot overflow whatever the token supply.
- **Payments.** Settlement, timeout and expiry only write `withdrawable[account]`. `withdraw`
  zeroes the credit, reduces `totalLiability`, then calls the token; a failed transfer reverts the
  whole call so the credit survives. Every state-changing function is `nonReentrant`.
- **Token calls.** A low-level call that accepts either no return data or ABI `true`; `false` or a
  revert becomes `TransferFailed`. The token address is validated to have code at construction.
- **No admin.** There is no owner, fee, pause, sweep, rescue, cancel or upgrade. Direct transfers
  to the arena are `surplus()` and are stuck by design.
- **Records.** Per-address wins/losses/draws/forfeits with the exact rules from the task. The
  README states explicitly that this is not Sybil-resistant.

## Checks actually run (offline)

All commands were run locally with `forge 1.7.1` and a cached `solc 0.8.26`, with no network
access needed by the build or tests.

```
forge build            Compiler run successful (lint warnings on block.timestamp are excluded via [lint])
forge test             8 suites, 128 tests passed, 0 failed, 0 skipped
forge fmt --check      clean
forge build --sizes    DuelArena runtime 8,157 B; DuelToken runtime 1,338 B (EIP-170 limit 24,576 B)
```

Test inventory by requirement:

| Requirement                                             | Where                                                                                    |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| All nine move pairs, credits and W/L/D records          | `DuelArena.t.sol::test_allNineMovePairs`, `testFuzz_settlementConservesPot`              |
| Open and designated opponents                           | `test_joinOpenDuelEscrowsMatchingStake`, `test_joinDesignatedDuelOnlyByDesignatedOpponent`, `testFuzz_designatedOpponentIsEnforced` |
| Nonce races, reuse, concurrent duels                    | `test_createRejectsReusedNonceAfterRace`, `test_createManyConcurrentDuelsBySameCreatorAreIsolated`, `testFuzz_onlyCurrentNonceIsAccepted` |
| Domain/replay separation by chain, arena, duel, player  | `test_commitmentFromAnotherChainIsRejected`, `...AnotherArena...`, `...AnotherDuel...`, `test_copiedCommitmentCannotBeRevealedByCopier`, `testFuzz_commitmentDomainSeparation` |
| Deadline boundaries incl. exact equality                | `test_joinExactlyAtDeadlineReverts`, `test_revealExactlyAtDeadlineReverts`, `test_timeoutOneSecondEarlyReverts`, `test_expireAtDeadlineExcludesJoinAtDeadline`, `testFuzz_joinDeadlineBoundary`, `testFuzz_revealDeadlineBoundary` |
| Invalid/duplicate/out-of-order actions                  | `test_revealRejectsInvalidMove`, `test_repeatRevealIsRejected`, `test_revealBeforeJoinIsRejected`, `test_secondJoinIsRejected`, `test_revealAfterSettlementReverts`, `test_lateRevealCannotPreemptTimeout` |
| Both timeout outcomes and unmatched expiry              | `DuelArenaTimeouts.t.sol` (sole revealer either side, double forfeit, expiry, no cancellation) |
| Double settlement / withdrawal                          | `test_timeoutCannotBeFinalizedTwice`, `test_expireCannotRunTwice`, `test_withdrawPaysCreditOnceAndClearsIt` |
| Escrow isolation between games                          | `test_escrowIsIsolatedBetweenDuels`, `test_withdrawDoesNotTouchOtherPlayersOrLiveEscrow` |
| Failed withdrawals and reentrancy                       | `DuelArenaAdversarialTokenTest` (return-false, revert, reentrant withdraw/create/finalize) |
| Conservation of balances and liabilities                | `test_conservationAcrossManyDuels`, invariants `balanceCoversLiability`, `liabilityMatchesGhostAccounting`, `tokenSupplyConserved`, deterministic campaign |
| Token supply and no privileged mint                     | `DuelToken.t.sol` (exact 10^27, 16 admin selectors rejected, deployer cannot mint, opcode scan) |
| Launch floor rehearsal                                  | `Deployment.t.sol` (CREATE2 from a factory on chain id 11155111, supply stays with factory, runtime size and F4/F2/FF scan) |

The invariant campaign runs 48 sequences of depth 64 with `fail_on_revert = true`, so every
handler call is a valid transition and any revert is a defect. The handler fast-forwards to
deadlines for timeout and expiry actions, and a seeded deterministic campaign asserts that joins,
settlements, timeouts, expiries and withdrawals were all reached and that the arena drains to zero.

## Self-review findings

Issues found and fixed while building:

1. The first invariant handler called a view on the arena between `vm.prank` and the target call,
   which consumed the prank and made the handler the caller. Fixed; the campaign then ran with
   zero reverts.
2. The first handler's random time warps outran the one-hour windows, so settlements were rarely
   reached. Fixed by bounding warps to 30 minutes, adding `revealBoth`/`playFullDuel` actions, and
   adding the deterministic campaign test that asserts each path was exercised.

Adversarial scenarios reasoned through and covered by tests: copied commitments, cross-duel and
cross-arena replays, chain-id replays, joining a designated duel from a third address, creator
joining their own duel, revealing before a join, late reveals, reveal after settlement, double
finalization, withdraw with no credit, hostile token returning false or reverting, reentrant
`withdraw`, reentrant `createDuel` during stake pull, reentrant `finalizeTimeout` during payout.

## Remaining limitations

- This is a builder's self-check, not an audit. The task requires a separate independent
  adversarial review of source, tests and manifest before release; that review should try to
  reproduce escrow, commitment, deadline and permission failures against the accepted commit.
- Only `DuelToken` is supported. Fee-on-transfer, rebasing and hook-bearing tokens are out of
  scope; `surplus()` reverts if the arena were ever below its liability, which cannot happen with
  the real token.
- Deadlines rely on `block.timestamp` and tolerate validator drift of seconds, not more.
- Funds can be locked for up to one join window (unmatched) or join plus reveal window (matched,
  opponent silent). There is no cancellation by design.
- A lost or predictable salt forfeits the duel; the README documents salt hygiene.
- Records are address-based and trivially farmable; they are not a reputation system.
- Timeout and expiry require someone to pay gas.
- The fuzz and invariant bounds (256 fuzz runs, 48×64 invariant calls) are sized to run quickly
  offline; deeper campaigns are cheap to run by raising the numbers in `foundry.toml`.

## Deployment parameters and operational responsibilities

| Item                      | Value / owner                                                                  |
| ------------------------- | ------------------------------------------------------------------------------ |
| Token                     | `DuelToken`, no constructor args, decimals 18, supply 10^27 to `msg.sender`    |
| Application               | `DuelArena`, constructorArgs `["$token", "3600", "3600"]`                       |
| Compiler settings         | solc 0.8.26, optimizer 200 runs, evm_version cancun, bytecode_hash none         |
| Pool                      | hookless, native ETH pair, fee 3000, tickSpacing 60, initialPrice 79228162514264337593543950336 |
| Manifest                  | written by the separate manifest assignment (`launch.json`, kind `evm_project`) |
| Deployment and broadcast  | the configured deployer only; contributors never read keys or broadcast        |
| Token distribution        | protocol reward claims via `MerkleDistributor` and the launch pool; no faucet   |
| Finalization gas          | players or any volunteer; nothing accrues to the caller                        |
| Independent review        | required before release; the protected floor alone does not establish game correctness |
