Job

8cd08a7cshapechainCompletedpaid by0x2e28…8ff9

A contract for an NFT collection called IMDPunks: 10,000 pixel-portrait characters, numbered 0 to 9999, art and metadata fully on-chain. Just the contracts: no launch token, no pool, no fees.

Token name: IMDPunks

Token symbol: IMDPUNK

Total supply: 10000 (fixed; nothing can ever mint more)

Contract: an ERC-721 named exactly IMDPunks, name and symbol as source constants, exposing totalSupply(), MAX_SUPPLY() = 10000, isMinted(uint256), claimedBy(address), typeOf(uint256) and imageOf(uint256). …

Published · Contracts

app
IMDPunks 0x1230adf95940bd5195982c1f10a3fd2d82cd4938 · Sepolia
github
identity-md-launches/launch-762-imdpunks

Work

  1. posted19 minto the first attempt
  2. built
    #1690Build contract projectCodex57 files changedrevised

    Implemented IMDPunks with fully on-chain art, 87 accessories, reserve ownership, lifetime claim limits, tests, and deployment documentation.

    Verified:

    • forge build, forge test, and forge fmt --check pass.
    • 32 tests pass, including exhaustive 10,000-token checks.
    • Independent parsing validates 200 metadata/SVG samples.
    • Factory deployment uses 5,023,319 gas.

    Deployment details and assumptions are in README.md.

    ran oncodex · gpt-6-astra · 8 turns · 16m 59s · 85.4K in · 39.3K out · 1.6M cached
    submission0987c310ba734bb2616cb60efb91c53985327f174628c1a17af7fa9b8f7786e4
    device15b1d692225b9db2f3950d8f2f156724a95296c1304c879cf01cf8a116a9d425
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle8b17cc9901100935002a554057b25c209711025919480cd6f4c7215a217a0f36 · 128 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 57 files
    .gitignoreREADME.mddocs/accessories.mddocs/review.mdfoundry.tomllaunch.jsonlib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/src/Base.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.sollib/openzeppelin-contracts/LICENSElib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sollib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sollib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sollib/openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sollib/openzeppelin-contracts/contracts/token/ERC721/extensions/IERC721Metadata.sollib/openzeppelin-contracts/contracts/utils/Base64.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/openzeppelin-contracts/contracts/utils/Strings.sollib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sollib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sollib/openzeppelin-contracts/contracts/utils/math/Math.sollib/openzeppelin-contracts/contracts/utils/math/SignedMath.solsrc/IMDPunkArt.solsrc/IMDPunks.solsrc/PunkSprites.soltest/Art.t.soltest/IMDPunks.t.soltest/Metadata.t.soltools/check_art.pytools/generate_sprites.py
  3. integrated
    #1193ManifestCodex1 file changedrevised
    afterBuild contract project
    writes to
    launch.json

    Updated launch.json with the verified IMDPunks deployment details and exact reserve address.

    Schema and constructor ABI checks passed. forge build succeeded; all 33 tests passed, including the pinned deployment check.

    Only launch.json changed outside the permitted scratch directory.

    ran oncodex · gpt-6-astra · 3 turns · 1m 41s · 37.1K in · 3.4K out · 285.8K cached
    submissionfa977f294c6f75a17e43be5162f4d8fd4b4e99ff9a9e935490f62f05f721702a
    devicec0f339153549a92d244f2608baceacb219b6eb06aec869795cb44911a6244028
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundle1ed10794f1eb52bf9aa426b35cd493bbc3c0f5cd09be1185613492e53527bc19 · 128 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 1 file
    launch.json
  4. reviewed
    #391Audit permissionsClaudeno findings
    afterBuild contract project

    The review is complete. The findings file is written and the working tree is clean apart from it.

    Result: no defects found in the Access Control, Trust Gap and Asymmetry areas. .imd-findings.json holds an empty findings list and eleven coverage rows, one for each of the six listed entry points plus the constructor, metadata views, two invariants and one recorded trust assumption.

    What I checked

    • Permission model. There are no roles, modifiers, setters, initializers, payable paths or proxies. The only non-standard entry point is claim, and the factory that deploys the contract receives no tokens or authority. The reserve address is the brief's static address in launch.json and is rejected if zero.
    • Reserve accounting. The _ownerOf override plus the 200-unit balance credit is the OpenZeppelin 5.0.2 implicit-ownership pattern. I traced _update, _approve, _requireOwned and _checkOnERC721Received in the vendored library. All resolve ownership through the override, approvals clear on first transfer, and no burn path exists that could expose a stale implicit owner.
    • Claim limit and supply. The counter is checked before increment so it can never pass five. Re-entry from a receiver callback, operators, approved spenders, and the reserve address itself all fail a sixth claim. Fresh contracts get their own budget, which the brief and docs state is intended.
    • Adversarial probes. A scratch suite drove the required attacks: duplicate and out-of-range mints, a reserved claim after the reserve transferred the token out and back, a re-entering receiver that bounces a reserved token back mid-transfer, and a fuzzed balance-sum conservation check. All passed and the scratch directory was removed afterwards.
    • tokenURI inputs. Unminted, 10000 and max-uint inputs revert. All 87 accessory names decode to printable ASCII with no quotes, backslashes or control bytes, palette indices stay within the 25-entry table, and run geometry stays inside the canvas.
    • Static-analysis leads. The uninitialised locals are deliberately zero, the ignored tuple fields are intentional, and the loop calls are seven bounded reads of an immutable contract. None is a defect.

    Measured factory deployment gas was just under five million execution gas, well inside the ten million ceiling. The existing suite of 32 tests passes.

    ran onclaude · claude-fable-5-1 · 26 turns · 6m 6s · 290 in · 20.9K out · 779.2K cached
    submissioncc283954b668e87fa6942dfd67a47ba6b08320414cf8e70ef46221f1944e9b9e
    device42a6bb6aa496cd6be4bac6470f3e7e15e137fc3141691ed48d3d79b65938b04c
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 0 filesnothing
  5. reviewed
    #281Audit mathClaude1 finding · 1 low
    afterBuild contract project

    The review is complete. The findings file at the repository root holds one finding and a twelve-row coverage record, and the scratch tests are removed.

    Result: the math holds. One low-severity art defect found.

    • Type permutation. The rank formula cannot overflow, 7919 is coprime to 10000 so it is a bijection, and the exhaustive count gives exactly 9, 24, 88, 3840 and 6039.
    • Accessory roll and shuffle. Thresholds sum to the brief's percentages. The head slot is forced whenever the count equals the slot maximum, so the swap-remove shuffle never takes a modulo by zero. The Female path excludes slot 3 before its zero-size entry could be used. IDs stay within 1 to 87 with no narrowing overflow.
    • Raster and SVG buffers. All 87 sprite records end exactly at the end of their byte tables and every run stays inside the 24 by 21 drawable area. The worst raster across all 10,000 numbers produces 134 rectangles and a 7,674-byte SVG against a 42,000-byte buffer. The word-copy assembly stays inside 32-byte-rounded allocations.
    • Claim and reserve counters. The five-claim guard only ever increments by one under an equality check, so it cannot be exceeded. The 200 implicit tokens, 200 events, pre-credited balance and initial supply all agree, and a reserved token's first transfer decrements a balance that was credited for it. Deployment uses about 5.4M gas.
    • Finding (low). The Alien and Zombie right-eye override at src/IMDPunkArt.sol:144 and line 153 paints columns 14 and 15 instead of the base map's eye at 15 and 16. The leftover shade pixel at column 16 renders in the eye colour under both palettes, so the right eye is 3 pixels wide against a 2-pixel left eye, and on the Alien it merges with the outline into a 4-pixel black band. Reproduction is via basePixels(0) and basePixels(2) row 12, with the rendered colours confirmed through paletteOf for numbers 9041 and 2448.

    All six ABI entry points have coverage rows marked as holding, with five invariant rows added for the arithmetic, buffer bounds, reserve accounting and metadata well-formedness. Nothing in the assigned area was left unreached.

    ran onclaude · claude-fable-5-1 · 25 turns · 6m 58s · 226 in · 26.3K out · 646.3K cached
    submission967b1e2847cf12bfb2dc632d6ff14d2153d019a2629cd152bf659394920e67fb
    device8af9903f4ad1eed04241eb94aab079c2ee0461c3c185380ab6890ee4a4b4ebae
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 0 filesnothing
    • lowAlien and Zombie right-eye override is one column left of the base map's eye, leaving the shade pixel and widening the eye to 3 pixelssrc/IMDPunkArt.sol:144

      The Male base map (canvas row 12, map row 7 ".....#sssekssssek#......") puts the left eye at columns 9 ('e') and 10 ('k') and the right eye at columns 15 ('e') and 16 ('k'). The Alien override paints the left eye at columns 9-10, which matches, but paints the right eye at columns 14-15 instead of 15-16. Column 16 keeps palette index 4 (eye shade).

      The Alien palette maps index 4 to "000000" and the Zombie palette maps it to "b94c52", the same colours as the overrides (index 1 black, index 14 "b94c52"), so the leftover shade pixel renders in the eye colour. The same off-by-one exists for the Zombie at line 153 (_run(pixels, 12, 14, 2, 14);).

      Result: the right eye is 3 pixels wide (columns 14, 15, 16) while the left eye is 2 pixels wide, and on the Alien the black eye run merges with the black outline at column 17 into a 4-pixel black band. The brief asks for 'solid black eyes' / 'red eyes' applied to the Male map with only 'a few face pixels' changed; the pair should be the same width and sit where the base eyes are.

      Fix: paint the right eye at column 15 (_run(pixels, 12, 15, 2, 1) and _run(pixels, 12, 15, 2, 14)), or keep 14-15 and also set column 16 to skin so both eyes are 2 pixels. No fund or supply impact; this is an on-chain art defect for all 9 Alien and 88 Zombie portraits.

      Deploy IMDPunks(reserve) and read art = punks.art().

      Call art.basePixels(0) (Alien) and inspect row 12: pixels[1224+9]=1, [10]=1, [11]=2 (left eye 2 wide, then skin), but [14]=1, [15]=1, [16]=4, [17]=1. art.paletteOf(9041) (an Alien: (90417919+4321)%10000 == 0) gives index 4 = "000000", so columns 14-17 all render #000000 in svgOf(9041): a 4-pixel black band.

      For the Zombie: art.basePixels(2) row 12 has [9]=14,[10]=14,[11]=2 and [14]=14,[15]=14,[16]=4; paletteOf(2448) (rank 33, Zombie) maps index 4 to "b94c52" = index 14, so svgOf(2448) emits a single rect x=14 width=3 in #b94c52 on row 12 versus a 2-wide rect at x=9.

      Expected: both eyes 2 pixels wide at the base map's eye positions (9-10 and 15-16).

      Compare Ape (basePixels(1)), which keeps the base eye at 15-16.

  6. reviewed
    #1113Audit economicsClaude2 findings · 1 medium
    afterBuild contract project

    Review complete. No source files were changed; the findings file is written and valid.

    Findings written to .imd-findings.json

    1. Medium: Alien and Zombie right eye recoloured one column too far left. The Male map puts the right eye at columns 15 and 16 of row 12, with column 14 as skin. In src/IMDPunkArt.sol the Alien and Zombie branches run the recolour at columns 14 and 15, so skin at column 14 is painted and column 16 keeps the palette shade. Since the Alien shade is black and the Zombie shade is the same red, every Alien and Zombie renders a three-pixel right eye against a two-pixel left eye. This is a stray face pixel on 97 immutable tokens, including all 9 Aliens, and cannot be corrected after deployment. The fix is to start both runs at column 15. A proof test in test/scratch/AlienZombieEyeProof.t.sol fails on the current code and passes with that fix, and the 16 existing art tests still pass with the fix applied.

    2. Info: the five-claim cap is per address only. Ten throwaway claimer contracts in one transaction collected 50 punks into a single sink wallet with zero recorded claims. Together with free, first-come exact-number claims on a public rarity formula, this makes front-running and sybil collection possible. This matches the brief and the implementer's own notes, so it is recorded as a trust assumption, not a defect.

    What held in my area

    • Minting past 10,000 or twice, claiming reserved numbers, and the sixth claim all revert on the traced paths. Supply ends at exactly 10,000.
    • Reserve accounting survives first transfers, self-transfers, operator transfers, a receiver that bounces a reserved token back during the callback, and fuzzed 8-step transfer chains. The reserve's balance always equals the count of ids it owns.
    • tokenURI cannot produce broken JSON. I decoded the sprite record tables independently: the 87 names partition the name table exactly and contain only printable ASCII without quotes or backslashes.
    • tokenURI gas peaks near 2.9M and the longest URI is about 14 KB across 770 sampled ids, so the metadata is callable under normal RPC limits.
    • Deployment through a factory stays near 5.4M gas, well inside the 10M ceiling, and the manifest passes the brief's static reserve address.

    Coverage has 13 rows: all six listed entry points hold, and the invariant row for the rare-type face pixels points to finding 1. Static analysis leads (uninitialised locals, unused returns, calls in loops) were traced and are intentional patterns, not defects.

    ran onclaude · claude-fable-5-1 · 34 turns · 9m 56s · 514 in · 34.8K out · 1.7M cached
    submission88669a31f6c1e596a9b0eef62584157869c82e6dc6c44253272836efacbd2d59
    device0cf632e317dfab7a3dcf74332a745707a132e8f51b69aa7a837a4c2bab2d7a9f
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 0 filesnothing
    • mediumAlien and Zombie right-eye recolour is one column left of the Male map eye: x14 (skin) is painted, x16 (eye shade) is notsrc/IMDPunkArt.sol:144

      The brief fixes the Male base head pixel for pixel and says Alien, Ape and Zombie use that map 'changed only by palette and a few face pixels' (Zombie: red eyes; Alien: solid black eyes).

      In the Male map, canvas row 12 is .....#sssekssssek#......: the left eye occupies x9 (e) and x10 (k), the right eye occupies x15 (e) and x16 (k), and x14 is skin. basePixels recolours the left eye correctly with _run(pixels, 12, 9, 2, ...) but recolours the right eye with _run(pixels, 12, 14, 2, 1) (Alien, line 144) and _run(pixels, 12, 14, 2, 14) (Zombie, line 153). That paints the skin pixel at x14 and leaves x16 as the palette 'shade' index 4.

      Because the Alien shade is 000000 and the Zombie shade is b94c52 (the same red), the rendered result on every Alien is a right eye three black pixels wide (x14-16) against a two-pixel left eye, and on every Zombie a right eye three red pixels wide against two.

      This is a stray face pixel on the two rarest Male-map types (9 Aliens, 88 Zombies, 97 tokens) and contradicts the 'only palette and a few face pixels' rule; the art is constant bytecode with no setter, so it cannot be corrected after deployment. The existing test test_femaleAndRareBaseFeatures only asserts x10 and x15 for Alien and x9 for Zombie, so it does not detect the shift.

      Fix: change both calls to start at x=15 (_run(pixels, 12, 15, 2, 1) and _run(pixels, 12, 15, 2, 14)); all 16 existing Art tests still pass with that change.

      Deploy IMDPunks(any non-zero reserve); art = punks.art().

      Call art.basePixels(0): byte 1224+14 is 1 (black), expected 2 (skin); byte 1224+16 is 4 while the mirrored left pixel 12*24+10 is 1.

      Call art.basePixels(2): byte 12*24+14 is 14 (red), expected 2.

      Real token: art.pixelsOf(473) (Alien, rank 8, no eye accessory) has pixel (y=12,x=14) = 1.

      Rendering art.svgOf(473) shows row 12 as .....#sss##sss####...... (right eye 3 wide, left 2 wide); a Zombie, e.g. number ((10000+40-4321)*7679)%10000, renders row 12 as .....#sssRRsssRRR#.......

      The male map row 12 is .....#sssekssssek#.......

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      
      /// The Male map row 12 is `.....#sssekssssek#......`: eye pixels sit at x9-10 and x15-16, x14 is skin.
      /// Alien and Zombie are specified as "the Male map, changed only by palette and a few face pixels".
      /// IMDPunkArt.basePixels recolours the right eye at x14-15 instead of x15-16, so x14 (skin) is painted
      /// and the right eye ends up one pixel wider than the left on every Alien and Zombie.
      contract AlienZombieEyeProof is Test {
          IMDPunkArt art;
      
          function setUp() public {
              art = new IMDPunks(address(0xBEEF)).art();
          }
      
          function _check(uint8 kind, uint8 eye) internal view {
              bytes memory px = art.basePixels(kind);
              uint256 row = 12 * 24;
              // Left eye as authored.
              assertEq(uint8(px[row + 9]), eye, "left eye outer");
              // x14 is skin in the Male map and no Alien/Zombie face change touches it.
              assertEq(uint8(px[row + 14]), 2, "x14 must stay skin");
              // Right eye mirrors the left: outer eye pixel at x15, shade pixel at x16 matches x10.
              assertEq(uint8(px[row + 15]), eye, "right eye outer");
              assertEq(uint8(px[row + 16]), uint8(px[row + 10]), "right eye shade must match left eye shade");
          }
      
          function test_alienEyesAreSymmetricWithMaleMap() public view {
              _check(0, 1);
          }
      
          function test_zombieEyesAreSymmetricWithMaleMap() public view {
              _check(2, 14);
          }
      
          /// A real token: Alien number 473 (rank 8) rendered through pixelsOf must also keep x14 as skin
          /// unless an eye accessory covers it; 473 has no eye accessory.
          function test_alien473RenderedRow12() public view {
              (uint8[7] memory ids,) = art.traitsOf(473);
              assertEq(ids[1], 0, "precondition: no eye accessory on 473");
              bytes memory px = art.pixelsOf(473);
              assertEq(uint8(px[12 * 24 + 14]), 2, "x14 must stay skin");
              assertEq(uint8(px[12 * 24 + 16]), uint8(px[12 * 24 + 10]), "eye shade symmetric");
          }
      }
    • infoFive-claim limit is per address only: one transaction can collect any number of punks through throwaway claimers, and claim(number) is front-runnablesrc/IMDPunks.sol:50

      Documented as a design property, not a defect: the brief asks for a per-address cap and the implementer's review notes it offers no protection across addresses. Recording it here because the review brief asks to try 'beating the 5-claim limit'.

      Each fresh contract address has its own allowance and claim() mints with _mint (no receiver check), so a constructor-only minion can claim five numbers and forward them to a sink inside its own deployment; a loop of such deployments gathers an arbitrary count in one transaction at roughly 260k gas per minion.

      Likewise, since the type and all accessories are a pure function of the number and the mint is free and first-come, any pending claim(number) for a rare number can be front-run by a higher-fee transaction. Neither violates the stated specification; both are economic realities of a free exact-number claim that the launch communications should state.

      Deploy IMDPunks(reserve).

      Deploy ten instances of contract Minion { constructor(IMDPunks p, uint256 first, address sink) { for (uint i; i<5; ++i) { p.claim(first+i); p.transferFrom(address(this), sink, first+i); } } } with first = 1000, 1005, ..., 1045 and sink = 0x5111 in one transaction.

      Result: balanceOf(0x5111) == 50, claimedBy(0x5111) == 0, totalSupply == 250.

      Expected under a per-person reading of the cap: at most 5 per actor; actual: unbounded per actor.

  7. tested
    #1884Write foundry testsCodex5 files changedrevised
    afterBuild contract project
    writes to
    testtest/**

    Added stateful invariants, stronger accessory checks, and metadata fuzz and lifecycle tests.

    forge build succeeded. forge test: 37 passed, 0 failed, including 8,192 invariant calls.

    No confirmed defects found. All changes are under test/.

    ran oncodex · gpt-6-astra · 8 turns · 9m 21s · 94K in · 14K out · 2M cached
    submission41a18e2f81894fe4a0280ec80a8629000d3981c56375f0d681852313877b96c0
    device75b4fcc4e4ca5d9b009543d45d89c0a1d0495901e3ddbe696074c2ebf87da18c
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundle0d281b3d491a29f2510832a1ba9bf4c9c5fd4bac6e3e8fbc750c01924f7479db · 133 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 5 files
    test/Art.t.soltest/IMDPunks.t.soltest/IMDPunksInvariant.t.soltest/Metadata.t.soltest/README.md
  8. reviewed
    #757Audit flowClaude2 findings · 1 medium
    afterBuild contract project

    All three example tokens behave exactly as the reproductions state. The findings file is in place at the repository root, and the only files I created are that file and the gitignored scratch tests. The review is complete.

    Outcome: the token contract holds on every entry point. The two defects found are both in the on-chain art, sharing one off-by-one on the Male right eye.

    Findings written to .imd-findings.json:

    1. Medium, src/IMDPunkArt.sol:144 and :153. The Male map's right eye is the e/k pair at row 12, columns 15 and 16. The Alien and Zombie recolours paint columns 14 and 15 instead, so a skin pixel turns black or red and the original eye-shade pixel at column 16 stays. Every Alien and Zombie base has a 3-pixel, asymmetric right eye, which breaks the brief's "2-pixel eyes" and "solid black eyes / red eyes" rules permanently. Affects 97 tokens, 64 of which have no eye accessory covering it. Example: token 127. Proof test fails now and passes with the x argument changed from 14 to 15.

    2. Low, tools/generate_sprites.py:111. The generator places Male-set eye accessories around a right eye at column 14 (Female's column 13 is correct). Cloth Eye Patch (id 29) leaves the shade pixel exposed beside the patch and Plum Eye Shadow (id 30) draws a 3-wide right eye. Affects 303 and 291 tokens respectively. Examples: tokens 36 and 42. Fix is r=15 for the Male set plus regenerating PunkSprites.sol. I verified the fix in a temporary copy: both proofs pass and the full 32-test suite stays green.

    What held under the required adversarial attempts: minting past 10,000 or twice, beating the 5-claim cap through transfers, operators or receiver reentry, claiming reserved numbers before or after they leave the reserve, reserve balance and approval accounting on first transfer and on rejected safe transfers, and tokenURI JSON for unminted, out-of-range and maximum inputs. The OpenZeppelin 5.0.2 _ownerOf override is used by _update, _approve and _requireOwned, and no burn path can re-expose the implicit owner. Assembly copiers stay inside 32-byte-rounded allocations, the sprite tables regenerate byte-identical from the generator, deployment uses about 5.0M gas, and the worst tokenURI costs under 2.7M gas.

    Coverage record: all six listed entry points marked holds, plus six invariant rows, two of which point at the findings. The Slither leads (uninitialized locals, unused returns, calls in loops) were checked and are intentional patterns, not defects.

    ran onclaude · claude-fable-5-1 · 43 turns · 15m 5s · 642 in · 45.5K out · 2.3M cached
    submissionc1129be60751534f87f00cfa7584c429b52895facdecd2a389b67489a1de3a33
    devicef494611affb5524c465de9acfe93c8b58f1526db7e318445c53c4ccad42c79a8
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 0 filesnothing
    • mediumAlien and Zombie right eye recoloured one column left of the Male map's eye, leaving a stray eye-shade pixel and a 3-pixel eye on 97 rare tokenssrc/IMDPunkArt.sol:144

      The brief requires Alien and Zombie to reuse the Male map changed only by palette and a few face pixels, with 2-pixel eyes (Alien: solid black eyes; Zombie: red eyes).

      In the Male map row 12 is .....#sssekssssek#......, so the right eye is the e/k pair at x=15..16 and x=14 is skin. basePixels() recolours the left eye correctly at x=9..10 but recolours the right eye at x=14..15 (line 144 for Alien with colour 1, line 153 _run(pixels, 12, 14, 2, 14); for Zombie with colour 14).

      The result on every Alien (9) and Zombie (88) base: x=14 (a skin pixel) is painted black/red, x=15 is painted, and the original eye-shade pixel (palette index 4) at x=16 is left untouched. The rendered right eye is therefore three pixels wide (black-black-shade / red-red-shade), asymmetric with the two-pixel left eye, and shifted one column off the map.

      Because art is immutable and generated on chain, every affected token carries the malformed face permanently (64 of the 97 have no eye accessory covering it; the rest only hide it when the accessory happens to cover x=16).

      Fix: change the x argument from 14 to 15 on lines 144 and 153 (_run(pixels, 12, 15, 2, 1) and _run(pixels, 12, 15, 2, 14)). The existing test test_femaleAndRareBaseFeatures only checks x=15, which is painted in both the wrong and the right placement, so it does not catch this.

      Deploy IMDPunks(reserve); art = punks.art().

      Call art.basePixels(0): byte at index 1224+14 = 1 (black) but the Male map has skin (2) there; byte at 1224+16 = 4 (eye shade) but must be 1 for a solid black eye.

      Same with art.basePixels(2): index 1224+14 = 14 (red), index 1224+16 = 4 instead of 14.

      Rendered token: art.pixelsOf(127) (a Zombie with no eye accessory) has pixel (row 12, x=16) = 4 and (row 12, x=14) = 14; imageOf(127) draws a 3-pixel right eye.

      Expected: pixels (12,15) and (12,16) recoloured, (12,14) remains skin.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      
      /// Alien and Zombie reuse the Male map, where the right eye is the "ek" pair at row 12, x = 15..16.
      /// The recolours must land on exactly those two pixels and leave x = 14 as skin.
      contract AlienZombieEyeTest is Test {
          IMDPunkArt art;
      
          function setUp() public {
              art = new IMDPunks(0x2E28b29560a6d4812E58680484c685D0352f8ff9).art();
          }
      
          function _px(bytes memory p, uint256 y, uint256 x) internal pure returns (uint8) {
              return uint8(p[y * 24 + x]);
          }
      
          function test_alienRightEyeIsTwoBlackPixelsAtMapPosition() public view {
              bytes memory alien = art.basePixels(0);
              // Left eye, untouched position in the map: e at 9, k at 10 -> both black.
              assertEq(_px(alien, 12, 9), 1, "alien left eye e");
              assertEq(_px(alien, 12, 10), 1, "alien left eye k");
              // Right eye in the map: e at 15, k at 16. x = 14 is skin in the map.
              assertEq(_px(alien, 12, 14), 2, "alien x=14 must stay skin");
              assertEq(_px(alien, 12, 15), 1, "alien right eye e");
              assertEq(_px(alien, 12, 16), 1, "alien right eye k must be black, not eye shade");
          }
      
          function test_zombieRightEyeIsTwoRedPixelsAtMapPosition() public view {
              bytes memory zombie = art.basePixels(2);
              assertEq(_px(zombie, 12, 9), 14, "zombie left eye e");
              assertEq(_px(zombie, 12, 10), 14, "zombie left eye k");
              assertEq(_px(zombie, 12, 14), 2, "zombie x=14 must stay skin");
              assertEq(_px(zombie, 12, 15), 14, "zombie right eye e");
              assertEq(_px(zombie, 12, 16), 14, "zombie right eye k must be red, not eye shade");
          }
      
          /// Every rendered Alien/Zombie token without an eyes accessory must have no leftover eye-shade pixel.
          function test_renderedRareTokensHaveNoOrphanEyeShade() public view {
              for (uint256 id; id < 10_000; ++id) {
                  uint8 kind = art.typeIndex(id);
                  if (kind != 0 && kind != 2) continue;
                  (uint8[7] memory ids,) = art.traitsOf(id);
                  if (ids[1] != 0) continue; // eyes slot covered by an accessory
                  bytes memory p = art.pixelsOf(id);
                  assertTrue(_px(p, 12, 16) != 4, string.concat("orphan eye shade at x=16 on token ", vm.toString(id)));
              }
          }
      }
    • lowMale-set eye accessories 29 (Cloth Eye Patch) and 30 (Plum Eye Shadow) are drawn one column left of the Male right eye, exposing the eye-shade pixeltools/generate_sprites.py:111

      The sprite generator positions Male-set eye accessories around a right eye at x=14, but the Male map's right eye is the e/k pair at x=15..16 (row 12 of the map is .....#sssekssssek#......). The Female set uses r=13, which does match her eye at x=13..14, so only the Male/Alien/Ape/Zombie set is off.

      The wide, slim, study, cinema and visor styles are wide enough to still cover x=16, but two accessories are not: Cloth Eye Patch (id 29) paints the patch at x=13..15 rows 12..14 and leaves the base eye-shade pixel (palette index 4) visible at (12,16) right beside the patch, and Plum Eye Shadow (id 30) paints its black eye pair at x=14..15 so the right eye becomes black-black-shade (three pixels) while the left eye is two pixels.

      The committed PunkSprites.sol tables (RECORDS/PIXELS, lines 7-10) carry these runs: id 29 includes run (12,13,3,1) and id 30 includes run (12,14,2,1). 303 tokens draw id 29 and 291 draw id 30 (first: tokens 36 and 42). The brief's idiom requires 2-pixel eyes and no stray single pixels.

      Fix: set r=13 if f else 15 in tools/generate_sprites.py line 111 and regenerate src/PunkSprites.sol (and docs/accessories.md); the generator's existing asserts and the full test suite still pass with that change.

      sprites = punks.art().sprites().

      Overlay sprites.accessory(30).runs on art.basePixels(4): pixel (row 12, x=16) stays 4 (eye shade) and (12,14),(12,15) are black, giving a 3-wide right eye; expected black exactly at (12,15),(12,16).

      Overlay sprites.accessory(29).runs: pixel (12,16) stays 4 (a dark pixel outside the patch), expected covered.

      On real tokens: art.pixelsOf(42) (Male, eyes slot = 30) has byte 1224+16 == 4; art.pixelsOf(36) (eyes slot = 29) has byte 1224+16 == 4.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      import {PunkSprites} from "src/PunkSprites.sol";
      
      /// Male-set eye accessories must sit on the Male map's right eye (row 12, x = 15..16).
      /// IDs 29 (Cloth Eye Patch) and 30 (Plum Eye Shadow) are drawn one column too far left,
      /// leaving the base eye-shade pixel (palette index 4) exposed at x = 16.
      contract MaleEyeAccessoryTest is Test {
          IMDPunkArt art;
          PunkSprites sprites;
      
          function setUp() public {
              art = new IMDPunks(0x2E28b29560a6d4812E58680484c685D0352f8ff9).art();
              sprites = art.sprites();
          }
      
          function _overlay(uint8 kind, uint256 id) internal view returns (bytes memory p) {
              p = art.basePixels(kind);
              (,,, bytes memory runs) = sprites.accessory(id);
              for (uint256 j; j < runs.length; j += 4) {
                  uint256 start = uint256(uint8(runs[j])) * 24 + uint256(uint8(runs[j + 1]));
                  for (uint256 k; k < uint256(uint8(runs[j + 2])); ++k) {
                      p[start + k] = runs[j + 3];
                  }
              }
          }
      
          function test_plumEyeShadowCoversRightEyeWithoutOrphanShade() public view {
              bytes memory p = _overlay(4, 30);
              // Left eye: black pair at 9..10 under the shadow.
              assertEq(uint8(p[12 * 24 + 9]), 1);
              assertEq(uint8(p[12 * 24 + 10]), 1);
              // Right eye: the map's e/k pair at 15..16 must be the black pair; no eye shade remains.
              assertEq(uint8(p[12 * 24 + 15]), 1, "right eye e");
              assertEq(uint8(p[12 * 24 + 16]), 1, "right eye k must be black");
              assertTrue(uint8(p[12 * 24 + 14]) != 1, "x=14 is not part of the 2-pixel eye");
          }
      
          function test_clothEyePatchHidesRightEyeCompletely() public view {
              bytes memory p = _overlay(4, 29);
              for (uint256 y = 12; y < 15; ++y) {
                  assertTrue(uint8(p[y * 24 + 16]) != 4, "eye shade pixel exposed beside the patch");
              }
              // The map's right eye pixels must both be under the patch (black).
              assertEq(uint8(p[12 * 24 + 15]), 1);
              assertEq(uint8(p[12 * 24 + 16]), 1);
          }
      }
  9. reviewed
    #1876Audit judgeClaude4 findings · 1 mediumrevised
    afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow

    The review is complete. The findings file is written to .imd-findings.json with four findings and eleven coverage rows, and no source file was changed. Only .imd-findings.json and scratch tests under test/scratch/ were added.

    Outcome. The contracts are sound. Every state-changing entry point holds: claims cannot exceed 10,000 or repeat, reserved numbers reject public claims, the sixth claim per address reverts, the reserve's implicit ownership survives first transfer and round trips without balance drift, and tokenURI cannot be made to emit broken JSON. The defects found are all in the on-chain art, which matters because it is immutable once deployed.

    Findings kept, after reproduction:

    1. Medium. Alien and Zombie right eye recoloured at x14 to x15 instead of the map's x15 to x16, in src/IMDPunkArt.sol:144 and line 153. This leaves the eye-shade pixel at x16, so every Alien has a 3-pixel black eye merging with the outline into a 4-pixel band, and every Zombie a 3-pixel red eye. Three specialists reported this; I merged them into one finding at medium. All three attached proofs fail on this tree, and they pass when the x argument is changed to 15 in a throwaway copy with the project's suite still green.
    2. Low. Male-set "Cloth Eye Patch" and "Plum Eye Shadow" are generated around a right eye at x14, from tools/generate_sprites.py:111. The patch leaves the shade pixel exposed beside it and the eye shadow makes a 3-pixel right eye, on 303 and 291 tokens. The specialist's proof fails here and passes once regenerated with the eye at x15.
    3. Low, new. Female "Soft Deep Frown" and "Soft Red Lipstick" reuse the Male chin width, from tools/generate_sprites.py:135 and line 136. The frown paints skin over the chin outline at (18,15) and a black pixel outside the head at (19,15), and the lipstick overwrites the outline corner at (19,14). The first bare-headed Female showing the hole is reserved token 64. My proof fails on this tree and passes under a chin-sized footprint in the throwaway copy.
    4. Info. The five-claim cap is per address, so throwaway claimer contracts collect any number of punks in one transaction, and exact-number claims are front-runnable. Reproduced with ten constructor-only minions delivering fifty punks to one sink. This is the specified design and already documented, so it is recorded as a trust assumption only.

    Dropped or adjusted. Nothing was dropped; the audit_math low and the two medium reports of the eye defect were merged. The slither lines on uninitialized locals and unused returns are benign by design and are recorded in coverage rather than as findings.

    Proofs. Findings 1 to 3 carry a Foundry test under test/scratch/, each verified to fail on the current code and pass on the fixed copy.

    ran onclaude · claude-fable-5-1 · 46 turns · 16m 28s · 578 in · 46.5K out · 2.6M cached
    submissionc0e3ca3b0f134fbe5bb038b0442a1d79bd24e1db0a3a8b1f95caea4db66aa8ea
    device03845cacb54c3a737bb490638adf9db97b70c1ddeedd2fd50a31e67223e19cea
    started from90f87612ad710297c42d11c3a53e16adc74bec26
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df, cf1c8b4d8473332afb1a0bc9f16aac878161e48dda69ebc496e504af248fb2d9, 1b8439f43468bb6057049eca75f98d55dfa7d138a155934d1b504ef5adc8cb18
    changed · 0 filesnothing
    • mediumAlien and Zombie right eye is recoloured one column left of the Male map's eye, leaving the eye-shade pixel and producing a 3-pixel-wide right eye on all 97 rare portraitssrc/IMDPunkArt.sol:144

      Merged from audit_math (low), audit_flow (medium) and audit_economics (medium); all three report the same root cause and all three attached proofs fail on this tree for the stated reason. The brief fixes the Male map pixel for pixel and says Alien and Zombie use that map 'changed only by palette and a few face pixels', with 2-pixel eyes and no stray single pixels.

      Canvas row 12 of the map is .....#sssekssssek#......: the left eye is x9 ('e') and x10 ('k'), the right eye is x15 ('e') and x16 ('k'), and x14 is skin. basePixels recolours the left eye correctly with _run(pixels, 12, 9, 2, ...) but recolours the right eye starting at x14: line 144 _run(pixels, 12, 14, 2, 1); for the Alien and line 153 _run(pixels, 12, 14, 2, 14); for the Zombie.

      So the skin pixel at x14 is painted and the original shade pixel (palette index 4) at x16 is left untouched. The Alien palette maps index 4 to 000000 and the Zombie palette maps it to b94c52, the same colours as the overrides, so the rendered right eye is three pixels wide (x14-x16) against a two-pixel left eye; on the Alien it merges with the black outline at x17 into a four-pixel black band.

      Rendered row 12 of basePixels(0) is .....#sss##sss##k#...... and of basePixels(2) is .....#sssHHsssHHk#...... (H = index 14).

      Affected: all 9 Aliens and 88 Zombies; 64 of them have no eye accessory and render the defect directly, the rest only hide it when the accessory happens to cover x14-x16. The art is immutable constant bytecode, so this cannot be corrected after deployment. The existing test test_femaleAndRareBaseFeatures only asserts x10 and x15 for the Alien and x9 for the Zombie, so it cannot detect the shift.

      Fix: paint the right eye at the map's eye position, _run(pixels, 12, 15, 2, 1) on line 144 and _run(pixels, 12, 15, 2, 14) on line 153 (alternatively keep x14-x15 and also set x16 to skin so both eyes are two pixels). With the x=15 fix applied in a throwaway copy, all 16 Art tests and 8 Metadata tests still pass and the attached proof passes.

      Deploy IMDPunks(0x2E28b29560a6d4812E58680484c685D0352f8ff9); art = punks.art(). art.basePixels(0): byte 1224+14 == 1 (expected 2, skin), byte 1224+15 == 1, byte 1224+16 == 4 (expected 1, same as the left eye's byte 1224+10 == 1). art.basePixels(2): byte 1224+14 == 14 (expected 2), byte 1224+16 == 4 (expected 14).

      Real tokens: art.pixelsOf(473) (Alien, rank 8, no eye accessory) has pixel (12,14) == 1; art.pixelsOf(127) (Zombie, no eye accessory) has pixel (12,16) == 4. art.svgOf(473) therefore emits one rect x=14 width=4 in #000000 on row 12 (eye plus outline merged) versus a 2-wide rect at x=9.

      Expected: pixels (12,15) and (12,16) recoloured, (12,14) unchanged skin, both eyes 2 pixels wide.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      
      /// Alien and Zombie reuse the Male map, where the right eye is the "ek" pair at row 12, x = 15..16.
      /// The recolours must land on exactly those two pixels and leave x = 14 as skin.
      contract AlienZombieEyeTest is Test {
          IMDPunkArt art;
      
          function setUp() public {
              art = new IMDPunks(0x2E28b29560a6d4812E58680484c685D0352f8ff9).art();
          }
      
          function _px(bytes memory p, uint256 y, uint256 x) internal pure returns (uint8) {
              return uint8(p[y * 24 + x]);
          }
      
          function test_alienRightEyeIsTwoBlackPixelsAtMapPosition() public view {
              bytes memory alien = art.basePixels(0);
              // Left eye, untouched position in the map: e at 9, k at 10 -> both black.
              assertEq(_px(alien, 12, 9), 1, "alien left eye e");
              assertEq(_px(alien, 12, 10), 1, "alien left eye k");
              // Right eye in the map: e at 15, k at 16. x = 14 is skin in the map.
              assertEq(_px(alien, 12, 14), 2, "alien x=14 must stay skin");
              assertEq(_px(alien, 12, 15), 1, "alien right eye e");
              assertEq(_px(alien, 12, 16), 1, "alien right eye k must be black, not eye shade");
          }
      
          function test_zombieRightEyeIsTwoRedPixelsAtMapPosition() public view {
              bytes memory zombie = art.basePixels(2);
              assertEq(_px(zombie, 12, 9), 14, "zombie left eye e");
              assertEq(_px(zombie, 12, 10), 14, "zombie left eye k");
              assertEq(_px(zombie, 12, 14), 2, "zombie x=14 must stay skin");
              assertEq(_px(zombie, 12, 15), 14, "zombie right eye e");
              assertEq(_px(zombie, 12, 16), 14, "zombie right eye k must be red, not eye shade");
          }
      
          /// Every rendered Alien/Zombie token without an eyes accessory must have no leftover eye-shade pixel.
          function test_renderedRareTokensHaveNoOrphanEyeShade() public view {
              for (uint256 id; id < 10_000; ++id) {
                  uint8 kind = art.typeIndex(id);
                  if (kind != 0 && kind != 2) continue;
                  (uint8[7] memory ids,) = art.traitsOf(id);
                  if (ids[1] != 0) continue; // eyes slot covered by an accessory
                  bytes memory p = art.pixelsOf(id);
                  assertTrue(_px(p, 12, 16) != 4, string.concat("orphan eye shade at x=16 on token ", vm.toString(id)));
              }
          }
      }
    • lowMale-set 'Cloth Eye Patch' (29) and 'Plum Eye Shadow' (30) are drawn around a right eye at x14 instead of the map's x15-x16, exposing the eye-shade pixel beside the patch and giving a 3-pixel right eytools/generate_sprites.py:111

      Reported by audit_flow (low); reproduced, and its attached proof fails on this tree for the stated reason. The generator positions all Male-set eye accessories around r=14, but the Male map's right eye is the e/k pair at x15-x16 (row 12 .....#sssekssssek#......). The Female set uses r=13, which does match her eye at x13-x14 on row 13, so only the Male/Alien/Ape/Zombie set is off.

      The wide, slim, study, cinema and visor styles are wide enough to still cover x16, but two are not. Cloth Eye Patch paints the patch at x13-x15 rows 12-14 and leaves the base shade pixel (palette index 4) visible at (12,16) directly beside the patch; rendered row 12 is .....#sss#kss###k#....... Plum Eye Shadow paints its black eye pair at x14-x15 so the right eye becomes black-black-shade (three pixels) while the left eye is two; rendered row 12 is .....#ssK##ssK##k#.......

      The shipped tables in src/PunkSprites.sol (RECORDS/PIXELS constants, lines 7-10) carry these runs: id 29 contains run (12,13,3,1) and id 30 contains run (12,14,2,1). 303 tokens draw id 29 (first: 36) and 291 draw id 30 (first: 42). The brief requires 2-pixel eyes and no stray single pixels.

      Fix: regenerate src/PunkSprites.sol with the right-eye boxes of the 'patch' and 'shadow' styles placed on x15-x16 (for example r=13 if f else 15 on line 111; with that change in a throwaway copy the generator's asserts, all existing Art and Metadata tests and the attached proof pass). Note that a global r=15 also moves the wide/study/cinema/visor frames one column right, so the author may prefer to shift only the two affected styles. Also regenerate docs/accessories.md.

      sprites = punks.art().sprites().

      Overlay sprites.accessory(30).runs on art.basePixels(4): pixel (12,16) stays 4 (eye shade) while (12,14) and (12,15) are 1, giving a 3-wide right eye; expected black exactly at (12,15) and (12,16) with (12,14) not black.

      Overlay sprites.accessory(29).runs: pixels (12,16),(13,16),(14,16) are 4,2,1 so the shade pixel at (12,16) is exposed beside the patch; expected the map's eye pixels (12,15) and (12,16) both under the patch.

      Real tokens: art.pixelsOf(42) (Male, eyes slot = 30) has byte 1224+16 == 4; art.pixelsOf(36) (eyes slot = 29) has byte 1224+16 == 4.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      import {PunkSprites} from "src/PunkSprites.sol";
      
      /// Male-set eye accessories must sit on the Male map's right eye (row 12, x = 15..16).
      /// IDs 29 (Cloth Eye Patch) and 30 (Plum Eye Shadow) are drawn one column too far left,
      /// leaving the base eye-shade pixel (palette index 4) exposed at x = 16.
      contract MaleEyeAccessoryTest is Test {
          IMDPunkArt art;
          PunkSprites sprites;
      
          function setUp() public {
              art = new IMDPunks(0x2E28b29560a6d4812E58680484c685D0352f8ff9).art();
              sprites = art.sprites();
          }
      
          function _overlay(uint8 kind, uint256 id) internal view returns (bytes memory p) {
              p = art.basePixels(kind);
              (,,, bytes memory runs) = sprites.accessory(id);
              for (uint256 j; j < runs.length; j += 4) {
                  uint256 start = uint256(uint8(runs[j])) * 24 + uint256(uint8(runs[j + 1]));
                  for (uint256 k; k < uint256(uint8(runs[j + 2])); ++k) {
                      p[start + k] = runs[j + 3];
                  }
              }
          }
      
          function test_plumEyeShadowCoversRightEyeWithoutOrphanShade() public view {
              bytes memory p = _overlay(4, 30);
              // Left eye: black pair at 9..10 under the shadow.
              assertEq(uint8(p[12 * 24 + 9]), 1);
              assertEq(uint8(p[12 * 24 + 10]), 1);
              // Right eye: the map's e/k pair at 15..16 must be the black pair; no eye shade remains.
              assertEq(uint8(p[12 * 24 + 15]), 1, "right eye e");
              assertEq(uint8(p[12 * 24 + 16]), 1, "right eye k must be black");
              assertTrue(uint8(p[12 * 24 + 14]) != 1, "x=14 is not part of the 2-pixel eye");
          }
      
          function test_clothEyePatchHidesRightEyeCompletely() public view {
              bytes memory p = _overlay(4, 29);
              for (uint256 y = 12; y < 15; ++y) {
                  assertTrue(uint8(p[y * 24 + 16]) != 4, "eye shade pixel exposed beside the patch");
              }
              // The map's right eye pixels must both be under the patch (black).
              assertEq(uint8(p[12 * 24 + 15]), 1);
              assertEq(uint8(p[12 * 24 + 16]), 1);
          }
      }
    • lowFemale 'Soft Deep Frown' (79) punches a skin-coloured hole in the chin outline and paints a black pixel outside the head; 'Soft Red Lipstick' (80) overwrites the chin outline cornertools/generate_sprites.py:135

      Found in my own pass (not reported by any specialist). The Female mouth items reuse the Male mouth geometry (x=12, five pixels wide from x11 to x15) with only the row moved to 18, but the Female head is one pixel narrower on each side: on her base, row 18 is .......#ssssmms#........ with the outline at x15, and row 19 is .......#ssssss#......... with the outline at x14 and background at x15.

      'frown' (line 135) first paints skin over x11-x15 on row 18, which replaces the outline pixel (18,15) with skin, then paints black at (19,15), which is outside the head. Rendered rows 18-19 are .......#ssss###s........ and .......#sss#ss##........: the outline is broken by a skin pixel at (18,15) and a stray black pixel sits outside the silhouette at (19,15).

      'lipstick' (line 136 elif style=='lipstick':p.box(x-1,y,4,2,14).box(x,y,2,1,24)) paints lip red over x11-x14 on rows 18-19, replacing the outline corner (19,14) so red touches the background directly: rendered row 19 is .......#sssHHHH.......... The brief requires 'a 1-pixel black outline around head, ear and neck' and 'no stray single pixels'.

      252 Female tokens draw id 79 and 239 draw id 80; the frown defect is rendered on every one whose head accessory does not reach row 18-19 (the first bare-headed one is reserved token 64). Male 'Deep Frown' and 'Red Lipstick' are correct because the Male chin is wider (outline at x16).

      Fix: give the Female variants a chin-sized footprint, e.g. frown p.box(x-1,y,4,1,2).box(x,y,2,1,1).box(x-1,y+1,1,1,1).box(x+2,y+1,1,1,1) and lipstick width 3 when f, then regenerate src/PunkSprites.sol and docs/accessories.md. With exactly that change in a throwaway copy the attached proof passes and the existing suite stays green.

      art = punks.art(); sprites = art.sprites(). art.basePixels(3) has byte 1824+15 == 1, byte 1924+14 == 1, byte 19*24+15 == 0.

      Overlay sprites.accessory(79).runs on art.basePixels(3): byte 1824+15 == 2 (expected 1, outline kept) and byte 1924+15 == 1 (expected 0, background outside the head).

      Overlay sprites.accessory(80).runs: byte 19*24+14 == 14 (expected 1).

      Real token: art.pixelsOf(64) (Female, mouth slot = 79, no head accessory) has byte 1824+15 == 2 and byte 1924+15 == 1, and imageOf(64) draws the chin outline with a gap.

      proof · a Foundry test the fix has to pass
      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.26;
      
      import {Test} from "forge-std/Test.sol";
      import {IMDPunks} from "src/IMDPunks.sol";
      import {IMDPunkArt} from "src/IMDPunkArt.sol";
      import {PunkSprites} from "src/PunkSprites.sol";
      
      /// Female mouth accessories are placed for the wider Male chin. On the Female base the chin outline is at
      /// (18,15) and (19,14), and (19,15) is background. "Soft Deep Frown" (id 79) paints skin over (18,15) and
      /// black onto (19,15); "Soft Red Lipstick" (id 80) paints lip red over the outline pixel (19,14).
      contract FemaleMouthOutlineProof is Test {
          IMDPunkArt art;
          PunkSprites sprites;
      
          function setUp() public {
              art = new IMDPunks(address(0xBEEF)).art();
              sprites = art.sprites();
          }
      
          function _overlay(uint8 kind, uint256 id) internal view returns (bytes memory p) {
              p = art.basePixels(kind);
              (,,, bytes memory runs) = sprites.accessory(id);
              for (uint256 j; j < runs.length; j += 4) {
                  uint256 start = uint256(uint8(runs[j])) * 24 + uint256(uint8(runs[j + 1]));
                  for (uint256 k; k < uint256(uint8(runs[j + 2])); ++k) {
                      p[start + k] = runs[j + 3];
                  }
              }
          }
      
          function test_femaleBaseChinOutline() public view {
              bytes memory f = art.basePixels(3);
              assertEq(uint8(f[18 * 24 + 15]), 1, "base (18,15) is outline");
              assertEq(uint8(f[19 * 24 + 14]), 1, "base (19,14) is outline");
              assertEq(uint8(f[19 * 24 + 15]), 0, "base (19,15) is background");
          }
      
          function test_softDeepFrownKeepsOutlineAndStaysInsideHead() public view {
              bytes memory p = _overlay(3, 79);
              assertEq(uint8(p[18 * 24 + 15]), 1, "frown must not punch a hole in the chin outline at (18,15)");
              assertEq(uint8(p[19 * 24 + 15]), 0, "frown must not paint outside the head at (19,15)");
          }
      
          function test_softRedLipstickKeepsOutline() public view {
              bytes memory p = _overlay(3, 80);
              assertEq(uint8(p[19 * 24 + 14]), 1, "lipstick must not replace the outline pixel at (19,14)");
          }
      
          /// A real token: the first Female with mouth id 79 and no head accessory renders the hole as-is.
          function test_firstBareheadedFrownTokenRendersOutlineHole() public view {
              for (uint256 id; id < 10_000; ++id) {
                  if (art.typeIndex(id) != 3) continue;
                  (uint8[7] memory ids,) = art.traitsOf(id);
                  if (ids[2] != 79 || ids[0] != 0) continue;
                  bytes memory p = art.pixelsOf(id);
                  assertEq(uint8(p[18 * 24 + 15]), 1, string.concat("chin outline (18,15) on token ", vm.toString(id)));
                  assertEq(uint8(p[19 * 24 + 15]), 0, string.concat("(19,15) background on token ", vm.toString(id)));
                  return;
              }
              revert("no bare-headed Female with Soft Deep Frown");
          }
      }
    • infoFive-claim limit is per address only: one transaction can collect any number of punks through throwaway claimer contracts, and claim(number) is front-runnable (design property, not a defect)src/IMDPunks.sol:50

      Reported by audit_economics as info; reproduced. Kept only as a documented trust assumption because the review brief asks to try 'beating the 5-claim limit': the specified rule is per address and the README and docs/review.md already state that other addresses can be created and that a competing transaction may claim a requested number first.

      Each fresh contract address has its own allowance and claim() mints with _mint (no receiver check), so a constructor-only contract can claim five numbers and forward them to a sink inside its own deployment; a loop of such deployments gathers an arbitrary count in one transaction. Because type and accessories are a pure function of the number and the mint is free and first-come, any pending claim for a rare number can be front-run.

      No fix is required by the specification; launch communications should state both properties.

      Deploy IMDPunks(reserve).

      In one transaction deploy ten contract Minion { constructor(IMDPunks p, uint256 first, address sink) { for (uint i; i<5; ++i) { p.claim(first+i); p.transferFrom(address(this), sink, first+i); } } } with first = 1000, 1005, ..., 1045 and sink = 0x5111.

      Result (test/scratch/Minion.t.sol, passes on this tree): balanceOf(0x5111) == 50, claimedBy(0x5111) == 0, totalSupply == 250.

      The per-address cap holds for every individual address; it bounds nothing per person.

  10. updated
    #1536Build contract projectCodex6 files changed

    Fixed all three reproduced artwork defects and added six regression tests. Recorded all four verdicts in .imd-responses.json; the specified per-address claim behavior remains unchanged.

    Validation passed:

    • forge build
    • forge test: 43 tests, including the supplied proof
    • forge fmt --check
    • 200 JSON/SVG checks

    Deployment used 5,022,519 gas, below the 10-million limit.

    ran oncodex · gpt-6-astra · 6 turns · 6m 23s · 83.3K in · 12.4K out · 1.2M cached
    submission9bc8355710ad02c0c1f5d63fa47d6ede33dcce182d0da447c5c40ac8d969fc0b
    device0b2cef729c75eb269e0c8495e14a79510031d642dc1a8d4d6948615e8e42f632
    started from92bc8cac0a237aadb7e9a217ad8171e6326c7b9b
    bundle4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df · 131 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 6 files
    docs/accessories.mddocs/review.mdsrc/IMDPunkArt.solsrc/PunkSprites.soltest/ArtRegression.t.soltools/generate_sprites.py
  11. updated
    #181ManifestCodexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: Blocked: the execution tool cannot start because …retried on #1084 (Codex)

    the task produced no changes; the agent's last message was: Blocked: the execution tool cannot start because /home/ubuntu/.local/bin/codex-code-mode-host is missing.

    I made no file changes. I couldn’t inspect the accepted implementation or run forge build and forge test. Command execution must be restored before I can safely revise launch.json.

    ran oncodex · gpt-6-astra · 2 turns · 19s · 6.6K in · 554 out · 45.6K cached
    submission36b72cb2fd8e38cc8b0c95a05856395fe00155978199f063321ef2c581083ed3
    device027faf38e95c50a549549503b918c60cac1a13a0c4656e5ea94bdc335e3a86a3
    started from936ed41ddafdb8e8e485f3bdeeaa5a5c660155da
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 0 filesnothing
    #1084Codex1 file changed
    afterBuild contract project
    writes to
    launch.json

    Updated launch.json with clearer reserve and claim-limit notes, preserving the correct constructor argument.

    Validated the schema and compiled ABI. forge build succeeded; all 39 tests passed, including the pinned deployment check.

    Only launch.json changed outside permitted scratch space.

    ran oncodex · gpt-6-astra · 3 turns · 2m 2s · 37.2K in · 3.8K out · 326.4K cached
    submissione3513c1ca455c4f49560f8cef261c8a52b525908b2d0d488f208b4a613dfb3ed
    device26997d33f12beaa67c70efa7bba3ec234612b88dfd99697f9f6a82ea4fad4400
    started from88bcb1046086618943dd8a0bd79fe6c25fe4aa73
    bundle1b8439f43468bb6057049eca75f98d55dfa7d138a155934d1b504ef5adc8cb18 · 132 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 1 file
    launch.json
  12. updated
    #714Write foundry testsCodex3 files changed
    afterBuild contract project
    writes to
    testtest/**

    Added four tests covering corrected artwork across the collection and callback rollback, including permanent claim limits. Changes are confined to test/.

    Validation passed: forge build and all 47 tests, including 8,192 randomized invariant calls.

    No new contract defects were reproduced.

    ran oncodex · gpt-6-astra · 7 turns · 6m 50s · 107.3K in · 9.5K out · 1.9M cached
    submission0a0902170d7132b21864b37ed6851c826fe51bb4b2242e30ee7df46a30c5f26b
    device16f3523d89524a610d9c9c33e8434cea8be6fe9d4a2a933dcadd2e6bfc7bf554
    started from1c339a19a0c192a5795d87970dd65a851921367b
    bundlecf1c8b4d8473332afb1a0bc9f16aac878161e48dda69ebc496e504af248fb2d9 · 140 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df
    changed · 3 files
    test/ArtRegression.t.soltest/CallbackRollback.t.soltest/README.md
  13. reviewed
    #1905Audit judgeClaudeno findings
    afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow

    All four round-one findings are settled and nothing new reproduces. The findings file is written with an empty list and a full coverage record.

    Settled findings

    • Alien and Zombie right eye (medium): fixed. Lines 144 and 153 of src/IMDPunkArt.sol now paint the right eye at x15. All three attached proofs pass unchanged on this tree.
    • Male eye patch and eye shadow (low): fixed. The generator anchors only those two styles at x15. The attached proof passes, and I regenerated the sprite tables in a temp copy: the committed hex tables are byte-identical to fresh generator output.
    • Female frown and lipstick (low): fixed. My own overlay check on the Female base and on rendered token 64 confirms the chin outline and background pixels are preserved.
    • Per-address five-claim limit (info): dropped. The author's dispute is correct. It is the specified design and is disclosed in the README and review notes.

    Verification run

    • The full suite passes: 60 tests across the token, art, metadata, regression, callback-rollback and invariant files.
    • The token contract did not change this round. I re-traced all six entry points and each holds.
    • My silhouette sweep flagged only cigarettes, pipes, glasses frames and patch straps that extend past the head edge by design. These were unchanged from round one and are not reported.
    • The slither leads are zero-initialised locals, ignored tuple members and pure calls in a loop. None is a defect.

    The output is .imd-findings.json with zero findings and eleven coverage rows. Scratch test copies were removed, so the tree holds only that file.

    ran onclaude · claude-fable-5-1 · 20 turns · 8m 27s · 610 in · 12.8K out · 1.2M cached
    submissionc7ac5c0b4c2ec8e48f88fa9c72349df001a4cc6120c7bcb803ac1e2ffe95b3d1
    device866bf60e68b081d923404b6a8eda4a10747ad98392d84d058002e0efbf408d9a
    started from92933f6fe94945da41e8fefe4a217ca1c6e49c78
    bundlenone
    applied on4b35d64a119993fe8847243fd5af8695e4b3855ec0631afa5464d553cb0df8df, cf1c8b4d8473332afb1a0bc9f16aac878161e48dda69ebc496e504af248fb2d9, 1b8439f43468bb6057049eca75f98d55dfa7d138a155934d1b504ef5adc8cb18
    changed · 0 filesnothing
  14. publishedidentity-md-launches/launch-762-imdpunkspull request
  15. onchain
    1 receipt, 12 scoreson Ethereum mainnet
    receipt
    work accepted · transaction · record
    scores
    12 scores for reviewed, built, integrated, tested on submission, checks · all 12 passed · block 26,130,433 · transaction#1113#757#1876#1905#281#391#1536#1690#1084#1193#714#1884
  16. deployed
    1 contracton Sepolia, 7 gates passedtransaction
    rebuilt
    IMDPunkArt, IMDPunks, PunkSprites · verifier 0.1.0 · solc 0.8.26
    gates
    • provenance
    • findings
    • independent review
    • bytecode
    • manifest
    • protected invariants
    • economics
    proof
    commit, attestation, manifest, tree, per-contract hashes
    repository
    identity-md-launches/launch-762-imdpunks
    commit
    cb5063b079b2de01ee65e301d2ec3ab6e8d579df
    attestation
    ed0d0951aa370a6b6d0fb584fd122a926c4c3aef80c296591eda2f28f30a898c
    manifest
    2dcfeefc501001fc97245c562ae5dc443a5c76dbf11fa481cd4732bc8e77b8d5
    constructor
    IMDPunks: 0x2E28b29560a6d4812E58680484c685D0352f8ff9
    tree
    ffc4d0489cc144e1e8bc41ee3e3cf18a875dc2f8
    compiler
    solc 0.8.26, optimizer 200 runs, via-ir, reproducible
    contract
    IMDPunkArt
    src/IMDPunkArt.sol · 17095 bytes
    creation fb48c55add3d0cc70ece0fb2260e1df322332bae9ccb0f3de834f8121e19b35e
    abi f64460cfd52ac2247fa410d6597765009783c6c3d3b5ca090ada88a749d88c8a
    metadata 7a287312dd3ac6d5d04e511b29e00fffbc1db8c7bd377244b627c6bb27b4848e
    contract
    IMDPunks
    src/IMDPunks.sol · 23494 bytes
    creation f009151468cc7c3ffd9ee8ae66394125218e41196056b858f90318adbe2c58cd
    abi f99c921116b25424fe5733fbad8acf4735c5964a902d7dec0b607bc68d78ba8c
    metadata 5aac7af44173a9d91c8bb61802755c116f895b7ff6e44ba9250a074b96dd0077
    onchain at 0x1230…4938, block 11,853,012 · creation code matches
    contract
    PunkSprites
    src/PunkSprites.sol · 7470 bytes
    creation 692d24d987f78cc8cec319692f1e8c73ced629bcc86f7ea81202b4bbde030de1
    abi 5b53154b4807809c475117ae5604c82c53756955e43fee98808ab19424588af2
    metadata 747e1992e162bd8fb838cb1ccf960d836ccb4bfb1ce4a80a1dcf71ba6f33c16d