{"assessments":[],"deployments":[{"attestationHash":"d218fc6c9de8b8b7869ef0664326793fd76a6adbe8a5f6beaa301da5e704ce18","chainId":4663,"contracts":[{"address":"0x30da521f63553ec3ea2dbb82c30581c00ccf9009","blockNumber":91565987,"name":"PixelFun","role":"other","txHash":"0xd7c0e30bc98b22a71eff1aa35df2362f131a295cbc981c764c74fc0511b72523"}],"id":"328add1b-9584-461f-aa63-47549a03bde7","manifestHash":"2ec05740701b17eb821f5a30977efbeefc7b9acd8270981b98284ad677c416a7","status":"live"}],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"7080a75d-d9a3-48da-a85c-9cb4e20a6f67","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"b2175bb9520d1fb169450f2c67b8f6c3c576aec42414a5146e7cc3d30eee1e78","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"42dc2b813b2d32cec8407ebd2f84146218ad75c04d76dc988f2deee1875fef67","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"83b10e37af63cbdf9472afa4c788c839e6f081b5adac8f6de55c71ab4cd09824","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"d7afd40a9946f379ca163b6c732eba6fcbca63ab8c58b5d6b5d81e38f52a6b18","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"38ad5e74d7458281c2cae3036d4dd6056e43fbbe245b9e41550033a432e63514","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"64c11b64ef55594d4e4ad72c23886bd7b9b65b26f56b5707fad5597708f7b520","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"24e3bf88075e26eb697bad2d70500e6256590334c3cc42c055088ccdceb992d9","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"7a47744b56755cfffb7cb620177b6684a8be33c6323af7de2bd86bfc5aff31ef","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"# Brief for the IMD swarm: PixelFun — a fully on-chain, ownerless pixel canvas that burns IMD\n\nBuild, audit and deploy ONE smart contract, \"PixelFun\", on Robinhood Chain, deployed unchanged after audit. It must genuinely use $IMD (token on Robinhood Chain: 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127): every pixel painted burns IMD. NO owner, NO admin, NO upgradeability, NO pause, NO privileged function of any kind. Do not add features beyond this brief.\n\n## What it is\nA shared 512x512 canvas (262,144 pixels) with a 32-color palette. Anyone paints pixels by spending a vault balance: 0.005 IMD per pixel, burned immediately. A 3-day round (\"epoch\") tracks the top 66 painters by pixels painted in that round; the top 66 of every finished round are the whitelist for a later NFT mint (the mint itself is OUT of scope). Everything needed to render the canvas and read the leaderboard must be readable from the contract alone, so a static website with only RPC access can show it.\n\n## Immutables / constants (constructor takes only the IMD address and MIN_PIXELS)\n- imd: the IMD token. PRICE = 5 * 10**(decimals-3) (0.005 token; require decimals >= 3, read decimals from the token).\n- WIDTH = 512, HEIGHT = 512, PALETTE_SIZE = 32, MAX_BATCH = 100, BOARD_SIZE = 66, EPOCH_LENGTH = 3 days.\n- EPOCH_START = block.timestamp at deployment. MIN_PIXELS = constructor argument (the owner of the launch will pass 20).\n- Burn: inspect the token first. If it has a permissionless burn(uint256), burn from the contract's own balance; otherwise send the tokens to 0x000000000000000000000000000000000000dEaD. Document which one and why (bridged tokens may restrict burn).\n\n## Vault\n- balanceOf(address) public: vault balance in IMD.\n- deposit(uint256 amount): SafeERC20 transferFrom, credit the amount actually received (balance difference), nonReentrant, emit Deposited(user, amount).\n- withdraw(uint256 amount): nonReentrant, checks-effects-interactions, returns only unspent balance, emit Withdrawn(user, amount). Always works.\n- Only msg.sender can spend their own vault balance. Nobody can move anyone else's funds.\n\n## Painting\n- paint(uint32[] ids, uint8[] colors): nonReentrant. Require equal lengths, 1..MAX_BATCH items, each id < WIDTH*HEIGHT (id = y*WIDTH + x), each color < PALETTE_SIZE. Duplicates in a batch are allowed (last wins, each is paid and counted).\n- cost = n * PRICE, deducted from the caller's vault balance and burned immediately.\n- Store pixel colors in contract storage, packed 32 pixels per 32-byte slot, 1 byte per pixel: 0 = never painted, 1..32 = colorIndex + 1.\n- Emit Painted(address indexed painter, uint64 indexed epoch, uint32[] ids, uint8[] colors, uint256 burned).\n- Counters: totalPixelsPainted (uint256), pixelsPaintedIn[epoch][painter] (uint32).\n\n## Rounds and leaderboard (all on-chain)\n- currentEpoch() = (block.timestamp - EPOCH_START) / EPOCH_LENGTH; epochStart(epoch), epochEnd(epoch) views.\n- For each epoch keep a board of at most BOARD_SIZE painters ranked by pixelsPaintedIn[epoch][painter]. A painter can enter the board only once they have >= MIN_PIXELS in that epoch. When the board is full, a newcomer displaces the lowest entry only if STRICTLY greater; ties are resolved in favor of whoever reached that count first (store the timestamp or block at which each entry last increased). Boards of finished epochs are frozen automatically because paint() only touches the current epoch.\n- Views: getBoard(epoch) returns (address[] painters, uint32[] counts) sorted by count descending, ties by earlier reach first, only filled entries; isWhitelisted(epoch, account) returns true if the account is on that epoch's board; epochEnded(epoch).\n- Keep the per-paint gas bounded (a 100-pixel batch must stay comfortably inside one transaction on the target chain). Choose an efficient board structure (for example packed entries with a cached minimum) and justify it.\n\n## Reads for the website\n- getPixels(uint256 startSlot, uint256 slotCount) returns bytes (slotCount*32 bytes, row-major pixel order); require a sane max slotCount; pixelAt(uint256 id) returns uint8.\n- PRICE, WIDTH, HEIGHT, PALETTE_SIZE, MAX_BATCH, BOARD_SIZE, MIN_PIXELS, EPOCH_LENGTH, EPOCH_START, imd, totalPixelsPainted as public getters.\n\n## Security and acceptance criteria\n- Tests must cover: deposit/withdraw accounting (including a fee-on-transfer token), paint cost and burned amount, bounds and palette checks, batch limits, insufficient balance, packed storage read/write correctness at slot boundaries, duplicate ids, epoch boundaries, board insert/displace/tie behavior, MIN_PIXELS gating, frozen boards after epoch end, reentrancy attempt, and the invariants: sum of vault balances <= token balance held, burned == PRICE * totalPixelsPainted.\n- If any requirement here cannot be met safely, say so and propose the smallest deviation. Do not silently drop it.\n- Verify the source on the explorer. Report the launch number, the final address, the audit report link and the final ABI so the website can integrate.\n- Requester / payout wallet for this launch: 0xc4a8ea10819d341d19d1dfbba7bb0e0f2295d952\n\nToken name: dsa\nToken symbol: fffa","parentJobId":null,"planHash":"e2d4cba6465d59221e8a61bb238d8c5858d414de57edeb151ee6884e607f796d","previousHash":"fb215844cafb586e46b4a13f0b154ea641e22e28ca8f13af08710165b88d2f06","projectId":"7080a75d-d9a3-48da-a85c-9cb4e20a6f67","publication":{"commit":"e880bd5931c48ca32e7de751471a4506bd25e6e4","deliveredAt":"2026-10-18T06:38:10.331Z","repoUrl":"https://github.com/identity-md-launches/launch-2148-dsa"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"2247bd72dfd219898cde93fbadaf83966581ab8a79d3f94d28cd504130d74ac3","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"42d4efe6302316e8aefdbf07d8ea2cc7615df651a5f25d1e00baeaf116a667eb","device":"7eaf1cd0fb3df712","findings":[],"hash":"24e3bf88075e26eb697bad2d70500e6256590334c3cc42c055088ccdceb992d9","nodeId":"a5af29c9-5ebb-46cc-a135-f534352dcba8","outcome":"completed","summary":"I wrote `launch.json` with one contract, `PixelFun`, the only contract in the accepted source. `forge build` succeeded and `forge test` passed all 21 tests with the verifier's fuzz seed. I didn't run the protected deployment test (`Contracts.protected.t.sol`); it needs the factory and init-code environment the verifier provides.\n\n```json\n{\n  \"kind\": \"evm_contracts\",\n  \"contracts\": [\n    { \"contract\": \"PixelFun\",\n      \"constructorArgs\": [\"0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127\", \"20\"] }\n  ],\n  \"notes\": \"...\"\n}\n```\n\n- **Constructor arguments:** they match `constructor(address imd_, uint256 minPixels_)` in order. The first is the IMD token address from the brief, written as a fixed address. The second is `MIN_PIXELS = 20`, the value the brief says the launch passes.\n- **No `$owner`:** the contract has no owner, admin, pause or upgrade path, so no argument needs one.\n- **Deployment:** the constructor doesn't call any other contract, so it won't fail when the launch checks run it on an empty chain. The price is read from the token's `decimals()` the first time it's used, then stored.\n- **Notes:** they run to 1,328 characters, under the 4,000 limit. They record that burns go to `0x…dEaD`, because the source says the bridged token's `burn(uint256)` couldn't be confirmed as open to anyone. They also cover the vault, packed pixel storage and the 66-place board per round.\n- **File checks:** the JSON parses and has only the `kind`, `contracts` and `notes` keys. `git status` shows `launch.json` as the only change. I haven't committed it.","treeHash":"ff22919362b03396c5f94049b53ce8243cb6c591","usage":{"cachedInputTokens":164037,"inputTokens":12,"model":"claude-opus-5-5","outputTokens":2558,"runtime":"claude","turns":7,"wallClockMs":47411}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"323aec17bfc8d652","findings":[{"citation":"resolved","description":"PixelFun itself has no owner, admin, pause or privileged function (verified: the only state-changing entry points are deposit, withdraw and paint, each scoped to msg.sender; there is no receive/fallback). The permission surface that remains is the token. Inspecting 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 on chain 4663 (public RPC named by the brief: Robinhood Chain) shows it is not a plain bridged ERC-20 but a LayerZero OFT with Ownable: owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7, selectors include transferOwnership, renounceOwnership, setPeer(uint32,bytes32), lzReceive (bridge mint path), setV4Config(address,address,bool), enableTransfers()/transfersEnabled() (currently true), blocked(address) (currently false for 0xdEaD, address(0) and the owner), updateName/updateSymbol/updateNameAndSymbol. There is NO burn(uint256) or burnFrom selector, so the builder's dead-address burn is the correct branch of the brief's rule, and an eth_call of transfer(0xdEaD, 1) from the owner succeeds (dEaD already holds ~28.4 IMD). Because every vault movement in PixelFun is a token transfer (lines 110, 123, 156), any owner-side transfer restriction applied to PixelFun's address (the blocked list the token exposes, or a future change to the transfer gate) makes deposit, withdraw and paint all revert at once and freezes vault balances until lifted. This is outside PixelFun's code and cannot be fixed inside it without violating the no-admin brief; it is recorded so the judge and website owner know the 'always works' guarantee is conditional on the IMD owner. The setter behind blocked(address) could not be identified from bytecode (source not available to this review), so whether it gates transfers today is unconfirmed; the owner role itself is confirmed.","line":123,"path":"src/PixelFun.sol","reproduction":"State: live token 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 on chain 4663. Observed: owner()=0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7; transfersEnabled()=true; blocked(0x000000000000000000000000000000000000dEaD)=false; no 0x42966c68 (burn(uint256)) selector in runtime code; eth_call transfer(0xdEaD,1) from owner returns true. Expected by the brief: an ownerless system. Actual: PixelFun is ownerless, but the asset it moves has an owner who can restrict transfers; if a transfer from PixelFun to msg.sender reverts at line 123, withdraw(amount) reverts for every user with no PixelFun-side recovery. No PixelFun code change is proposed; document it in the README/website as a trust assumption.","severity":"info","snippet":"        imd.safeTransfer(msg.sender, amount);","title":"Trust assumption: the IMD token on Robinhood Chain is owner-controlled (LayerZero OFT), so 'withdraw always works' depends on the token owner, not only on PixelFun"},{"citation":"resolved","description":"Asymmetry between the two bound checks of the same range guard: slotCount is checked directly, but startSlot is only checked through the checked sum startSlot + slotCount, which overflows (Solidity 0.8 checked arithmetic) before the comparison runs. The call still reverts, so no state or funds are affected and the website only ever passes startSlot < 8192; the revert reason is just not the documented custom error. Cosmetic. Minimal fix: check startSlot >= SLOT_COUNT first, or compare startSlot > SLOT_COUNT - slotCount after the slotCount check.","line":278,"path":"src/PixelFun.sol","reproduction":"Call getPixels(type(uint256).max, 1) (or any startSlot > 2^256 - slotCount). Expected: revert BadRange(). Actual: revert Panic(0x11) arithmetic overflow. Reproduced with a scratch Foundry test: vm.expectRevert(PixelFun.BadRange.selector); pf.getPixels(type(uint256).max, 1); fails with 'panic: arithmetic underflow or overflow (0x11) != BadRange()'.","severity":"info","snippet":"        if (slotCount == 0 || slotCount > MAX_READ_SLOTS || startSlot + slotCount > SLOT_COUNT) revert BadRange();","title":"getPixels reverts with Panic(0x11) instead of BadRange() when startSlot is near uint256 max"}],"hash":"38ad5e74d7458281c2cae3036d4dd6056e43fbbe245b9e41550033a432e63514","nodeId":"4f60ae62-3f5a-45cf-a027-6defad06c9c7","outcome":"completed","summary":"The review is complete and `.imd-findings.json` is written at the repository root. No source files were changed; only the findings file and throwaway tests under `test/scratch/` were created.\n\n**Outcome:** PixelFun holds up in the Access Control, Trust Gap and Asymmetry areas. I found no defect that loses funds, misdirects funds, or breaks a stated guarantee. The file carries two info-level notes and nine coverage rows.\n\n**What I verified**\n\n- **Permission surface.** The only state-changing entry points are deposit, withdraw and paint, each scoped to msg.sender. There is no owner, initializer, receive or fallback. A scratch test confirms a third party cannot withdraw or spend another user's vault balance and that ETH or unknown selectors revert.\n- **Reentrancy lead from aderyn.** A token whose transferFrom re-enters deposit, withdraw or paint is blocked by the guard in all three cases. A read-only probe mid-deposit shows balances in the conservative direction, so the sum-of-vaults invariant never overstates.\n- **Deposit and withdraw symmetry.** Both write balanceOf and totalDeposited in mirror directions, and paint mirrors them too. Withdraw never depends on the token's decimals, so it keeps working even if the price read fails.\n- **Leaderboard.** I wrote an independent reference model of the brief's rules and fuzzed it against the contract over 300 runs with 80 painters and up to 200 paints each. Membership, counts and exact order matched every time, including displacement, ties by earlier reach, MIN_PIXELS gating and displaced painters re-entering. I also argued why the strictly-greater rule is sound: a board entry tying a newcomer's count always reached it earlier.\n- **Live token inspection.** The brief required inspecting IMD for a permissionless burn, which the builder could not do. On Robinhood Chain the token is a LayerZero OFT with no burn or burnFrom selector, decimals 18, and a simulated transfer to the dead address succeeds. The builder's dead-address burn is therefore the correct branch of the brief's rule.\n\n**Findings recorded**\n\n1. **Info, trust assumption.** The IMD token has an owner and exposes a blocklist view and a transfer-enable switch. Every vault movement is a token transfer, so an owner-side restriction on PixelFun's address would freeze deposits, paints and withdrawals together. This lives outside PixelFun and cannot be fixed inside it under the no-admin brief. It is recorded so the website and the judge know the \"withdraw always works\" guarantee is conditional on the token owner.\n2. **Info, cosmetic.** `getPixels` with a startSlot near the uint256 maximum reverts with a Panic instead of the documented BadRange error, because the sum overflows before the comparison. Reproduced with a failing scratch test. No state impact.\n\n**Not reached:** the setter behind the token's `blocked(address)` view could not be identified from bytecode, so whether it gates transfers today is unconfirmed. The owner role itself is confirmed on chain.","treeHash":null,"usage":{"cachedInputTokens":1154308,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":31134,"runtime":"claude","turns":29,"wallClockMs":499843}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d7e6877fd9e388bb","findings":[{"citation":"resolved","description":"The brief defines PRICE as an immutable computed from the token's decimals at deployment. Because the launch rehearses constructors on an empty chain, the constructor cannot call the token, so the author defers the decimals() read and the decimals >= 3 check to the first deposit()/paint() and caches it (README documents this deviation). Consequence of the deviation: deployment always succeeds even when imd_ is wrong (EOA, non-token, or a token with decimals < 3 or > 77). In that state every deposit() and paint() reverts with UnsupportedDecimals (or the raw decimals() revert) forever, and since the contract is ownerless there is no remedy except redeploying. No funds are at risk because deposit() reverts before pulling tokens. This is reported as a documented, intentional deviation so the judge and the manifest step treat the imd_ constructor argument (0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127) as the single value that must be verified before launch; it is not a code defect. The caching itself is sound: PRICE() returns the live value until the first state-changing call and the cached value afterwards, and 100 * 5 * 10**74 cannot overflow uint256, which is why d > 77 is rejected.","line":108,"path":"src/PixelFun.sol","reproduction":"new PixelFun(address(tokenWithDecimals2), 20) succeeds; then deposit(1) from any account reverts UnsupportedDecimals() (existing test_lowDecimalsRejected shows this). Same with imd_ = an EOA: deposit reverts because decimals() returns no data. Expected per brief: constructor reverts at deployment when decimals < 3; actual: deployment succeeds and the contract is dead on arrival. Scratch check also confirmed: with decimals()=6 at first deposit and later changed to 18, PRICE() stays 5e3 (cached), and decimals()=78 reverts while 77 yields 5*10**74.","severity":"info","snippet":"        _price(); // validates the token's decimals once","title":"PRICE is derived from the token's decimals on first deposit/paint, not at construction; a wrong or <3-decimal IMD address yields a permanently unusable deployment that nothing can repair"}],"hash":"42dc2b813b2d32cec8407ebd2f84146218ad75c04d76dc988f2deee1875fef67","nodeId":"435023ef-9661-4d71-96bf-25dad5f27c8b","outcome":"completed","summary":"The review is complete and the findings record is written to `.imd-findings.json` in the repository root.\n\n**Result: no defects of medium or higher severity in the control-flow area.** The one entry recorded is informational.\n\n**What I covered**\n\n- **Execution trace** of all three entry points. Deposit credits the measured balance difference, withdraw follows checks-effects-interactions, and paint deducts cost before any write and burns last. Re-entry into deposit, paint and withdraw from inside the token's transfer hook was tested on both inbound and outbound paths and reverts with the guard error every time.\n- **Board logic** under First Principles. I wrote a differential fuzz comparing the on-chain board against a naive reference model over 300 runs with up to 200 paints by 90 painters competing for 66 slots. The board always matched the top-66 by count then earliest reach, and the whitelist view agreed. I also proved the cached minimum index can never go stale, since a non-minimum entry's count strictly increases and so it never drops below the minimum.\n- **Periphery.** The packed pixel read-modify-write, the assembly in getPixels, the price cache, the decimals bounds, and the vendored OpenZeppelin 5.1.0 guard and SafeERC20 all behave as assumed.\n- **Static-analysis leads.** The slither timestamp line is the intended epoch clock. The aderyn reentrancy line is the balance-difference pattern under nonReentrant and is not exploitable.\n\n**The one informational entry.** PRICE is derived from the token's decimals on first use rather than in the constructor, because the launch rehearses constructors on an empty chain. The author documents this deviation. Its consequence is that a wrong or sub-3-decimal token address deploys successfully and leaves the contract permanently dead, with no funds at risk. I flagged it so the manifest step treats the token address as the value to verify before launch.\n\n**Coverage record.** Eight rows: the three entry points, the board invariant, the two conservation invariants, the packed-storage views, and the static-analysis leads, all marked as holding. Scratch tests were removed since no finding needed a proof file.","treeHash":null,"usage":{"cachedInputTokens":911040,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":26370,"runtime":"claude","turns":24,"wallClockMs":396119}},{"artifacts":[],"attempt":1,"bundleHash":"1ff2f9cff0c5b141d89f039005eca784023ac92d771a460524d74ebd240453ab","device":"7f1df2a55552de06","findings":[],"hash":"64c11b64ef55594d4e4ad72c23886bd7b9b65b26f56b5707fad5597708f7b520","nodeId":"524d16b5-b0b2-4ec4-ad9c-2e8e49fb8a9f","outcome":"completed","summary":"I've built the PixelFun contract (`src/PixelFun.sol`) with tests, a README and the Foundry setup at the repository root. It has no owner, admin, pause or upgrade path, and its constructor takes only the IMD address and `MIN_PIXELS` (20 at launch). `forge build` and `forge fmt --check` pass. `forge test` passes 21 of 21 with the verifier's fuzz seed. Everything is committed as `3da3183`; forge-std and OpenZeppelin v5.1.0 are copied into `lib/` as plain files, not submodules.\n\n**One deviation from the brief:** I couldn't check whether IMD has a permissionless `burn(uint256)`. The Robinhood Chain RPC failed the TLS handshake from this machine. So the contract sends burned tokens to `0x…dEaD` instead, which works for any ERC-20. The README explains this. If someone confirms IMD has a permissionless burn, switching is a small change, but it has to happen before the audit since the contract deploys unchanged.\n\nOther points that differ from a literal reading of the brief:\n- **`PRICE` is a view function, not a stored constant.** The launch runs the constructor on an empty chain, so the constructor can't call the token. The contract reads `decimals()` on the first deposit or paint, checks it is at least 3, and caches it. A token with fewer than 3 decimals makes deposit and paint revert.\n- **Ties use a counter, not a timestamp.** Each time an entry's count goes up it gets the next number from a global counter, so \"who reached it first\" is exact even inside a single block.\n- **Board structure:** 66 one-slot entries (address, count, sequence number) plus a cached index of the lowest entry. Updating a painter already on the board, or turning away a newcomer, costs a fixed amount. The 66 entries are only rescanned when the lowest one can change. A 100-pixel batch with a full board measured under 3.5M gas.\n- **Values I chose:** `getPixels` returns at most 1,024 slots per call, so the website needs 8 calls for the whole canvas. I added three read-only extras: `BURN_ADDRESS`, `totalBurned` and `totalDeposited`.\n- **Limit:** a painter's per-round count is `uint32`, as the brief says, so going past about 4.29 billion pixels in one round reverts. That would cost over 21M IMD.\n\nThe tests cover every item in your list, including a fee-on-transfer token, a token that tries to re-enter, slot boundaries, tie and displacement order, and a fuzz test of the two invariants.\n\n**What I can't report:** the launch number, final address, explorer verification and audit report link. Those come from IMD's launch after acceptance; this job deploys nothing. An independent audit is still needed before release, since passing tests are not an audit. The ABI is in `out/PixelFun.sol/PixelFun.json` after `forge build`, and the README lists every function and event the website needs.","treeHash":"209515a334b11161e3195bbdb1968e5d24266367","usage":{"cachedInputTokens":434600,"inputTokens":18,"model":"claude-opus-5-5","outputTokens":17830,"runtime":"claude","turns":9,"wallClockMs":170287}},{"artifacts":[],"attempt":1,"bundleHash":"9afe9a65e604fa070c7971c192826a52ce8ac5c5c8f906e58fa329baebb323ff","device":"73d25b5e0857cef5","findings":[{"description":"PixelFun credits fee-on-transfer deposits correctly, but paint increments totalBurned and emits the nominal cost before transferring that cost to BURN_ADDRESS. SafeERC20 checks call success, not the amount actually received by the burn address. With a recipient-deducted transfer fee paid to a spendable collector, part of every paint cost remains in circulation. Therefore actual burned tokens do not equal PRICE * totalPixelsPainted even though the internal counters agree. This is a demonstrated incompatibility with outgoing transfer fees, not a claim that the production IMD token currently charges such a fee; its live behavior has not been verified here. The smallest safe change is to verify that the full required amount was irrecoverably burned and revert the entire paint if it was not, explicitly documenting any unsupported outgoing fee behavior. A verified permissionless burn path is another option if the actual IMD supports it. Do not debit unrelated depositors to compensate for a burn-transfer fee.","line":156,"path":"src/PixelFun.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PixelFun} from \"src/PixelFun.sol\";\n\n// Standard recipient-deducted 1% transfer fee goes to a spendable collector, not a burn.\ncontract OnePercentFeeProofToken {\n    uint8 public constant decimals = 18;\n    address public immutable collector;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(address collector_) { collector = collector_; }\n    function mint(address to, uint256 amount) external { balanceOf[to] += amount; }\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n    function transfer(address to, uint256 amount) external returns (bool) {\n        _transfer(msg.sender, to, amount);\n        return true;\n    }\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        _transfer(from, to, amount);\n        return true;\n    }\n    function _transfer(address from, address to, uint256 amount) internal {\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount - amount / 100;\n        balanceOf[collector] += amount / 100;\n    }\n}\n\ncontract PixelFunBurnFeeProof is Test {\n    function test_PaintMustActuallyBurnTheReportedFullPrice() public {\n        address collector = makeAddr(\"fee collector\");\n        OnePercentFeeProofToken token = new OnePercentFeeProofToken(collector);\n        PixelFun canvas = new PixelFun(address(token), 20);\n        token.mint(address(this), 1 ether);\n        token.approve(address(canvas), 1 ether);\n        canvas.deposit(1 ether);\n        assertEq(canvas.balanceOf(address(this)), 0.99 ether);\n        uint256 collectorBefore = token.balanceOf(collector);\n        try canvas.paint(new uint32[](1), new uint8[](1)) {\n            assertEq(canvas.totalPixelsPainted(), 1);\n            assertEq(canvas.totalBurned(), 5e15);\n            // A successful paint must burn its full reported cost; rejection is safe if this token is unsupported.\n            assertEq(token.balanceOf(canvas.BURN_ADDRESS()), 5e15, \"actual burn is smaller than the paid/reported burn\");\n            assertEq(token.balanceOf(collector), collectorBefore, \"paint cost leaked to a spendable fee collector\");\n        } catch {\n            assertEq(canvas.balanceOf(address(this)), 0.99 ether);\n            assertEq(token.balanceOf(address(canvas)), 0.99 ether);\n            assertEq(canvas.totalPixelsPainted(), 0);\n            assertEq(canvas.totalBurned(), 0);\n            assertEq(canvas.pixelAt(0), 0);\n            assertEq(token.balanceOf(canvas.BURN_ADDRESS()), 0);\n            assertEq(token.balanceOf(collector), collectorBefore);\n        }\n    }\n}","reproduction":"Deploy the self-contained 18-decimal token in the proof, which deducts a 1% fee from the recipient amount and credits a separate fee collector. Deploy PixelFun(token, 20). Mint 1e18 to the test caller, approve it, and deposit 1e18: vault credit is correctly 990000000000000000. Call paint([0], [0]). Expected: a successful paint irrecoverably burns 5000000000000000 units, or rejects the unsupported transfer atomically. Actual: the call succeeds, totalPixelsPainted=1, totalBurned=5000000000000000, but BURN_ADDRESS receives only 4950000000000000; the collector gains 50000000000000 spendable units from painting. Reproduced with forge 1.8.3 by saving proof to test/scratch/PixelFunBurnFeeProof.t.sol and running forge test --match-path test/scratch/PixelFunBurnFeeProof.t.sol -vv. The exact burn assertion fails with 4950000000000000 != 5000000000000000.","severity":"medium","title":"Outgoing transfer fees make successful paints burn less than PRICE while reporting the full burn"}],"hash":"7a47744b56755cfffb7cb620177b6684a8be33c6323af7de2bd86bfc5aff31ef","nodeId":"b636dd23-1e2d-4873-92bc-a35afe157169","outcome":"completed","summary":"Added failure-path, fuzz, and stateful invariant tests. `forge build` and seeded `forge test` pass: **56 tests, 73,728 invariant calls**.\n\nReported one medium-severity defect in `.imd-findings.json`, with a reproduced failing proof: outgoing transfer fees can make actual burns smaller than reported. Live IMD fee behavior remains unverified.","treeHash":"432ceb8eb9f50ae44a774d8a517b76d5e1294bf5","usage":{"cachedInputTokens":3968640,"inputTokens":129524,"model":"gpt-6-astra","outputTokens":36325,"runtime":"codex","turns":12,"wallClockMs":1122592}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"357c46e3781993d4","findings":[{"citation":"resolved","description":"Merged from audit_math (info) and write_foundry_tests (medium). deposit() credits the balance difference, so inbound fees are handled, but paint() adds `cost` to totalBurned (line 138) and emits `cost`, then transfers `cost` to 0x...dEaD without measuring what arrived. SafeERC20 only checks call success. With a token that deducts a fee from the recipient, the dead address receives cost - fee while the counters and the event report cost, so the brief's invariant `burned == PRICE * totalPixelsPainted` holds for the counter but not for tokens actually removed from circulation. Vault solvency is unaffected (totalDeposited == token.balanceOf(PixelFun) still holds because the fee leaves the contract's own balance). Severity recalibrated to low: I verified over RPC that the deployed IMD token (0x5F7B..7127, chain 4663, decimals 18, LayerZero-OFT 'Identity.md') charges no transfer fee, so on the actual launch the counter is exact; the gap is only reachable with a different token. Recommended fix is documentation: state in README/NatSpec that totalBurned is the amount debited from painters and sent to the dead address, exact for IMD, and that fee-on-transfer tokens are unsupported. Measuring the dead-address balance and reverting on a shortfall is the alternative the tester proposes, but under a fee token that would make every paint revert forever on an ownerless contract, so it should only be adopted if that behaviour is wanted.","line":156,"path":"src/PixelFun.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PixelFun} from \"src/PixelFun.sol\";\n\n// Standard recipient-deducted 1% transfer fee goes to a spendable collector, not a burn.\ncontract OnePercentFeeProofToken {\n    uint8 public constant decimals = 18;\n    address public immutable collector;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(address collector_) { collector = collector_; }\n    function mint(address to, uint256 amount) external { balanceOf[to] += amount; }\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n    function transfer(address to, uint256 amount) external returns (bool) {\n        _transfer(msg.sender, to, amount);\n        return true;\n    }\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        _transfer(from, to, amount);\n        return true;\n    }\n    function _transfer(address from, address to, uint256 amount) internal {\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount - amount / 100;\n        balanceOf[collector] += amount / 100;\n    }\n}\n\ncontract PixelFunBurnFeeProof is Test {\n    function test_PaintMustActuallyBurnTheReportedFullPrice() public {\n        address collector = makeAddr(\"fee collector\");\n        OnePercentFeeProofToken token = new OnePercentFeeProofToken(collector);\n        PixelFun canvas = new PixelFun(address(token), 20);\n        token.mint(address(this), 1 ether);\n        token.approve(address(canvas), 1 ether);\n        canvas.deposit(1 ether);\n        assertEq(canvas.balanceOf(address(this)), 0.99 ether);\n        uint256 collectorBefore = token.balanceOf(collector);\n        try canvas.paint(new uint32[](1), new uint8[](1)) {\n            assertEq(canvas.totalPixelsPainted(), 1);\n            assertEq(canvas.totalBurned(), 5e15);\n            // A successful paint must burn its full reported cost; rejection is safe if this token is unsupported.\n            assertEq(token.balanceOf(canvas.BURN_ADDRESS()), 5e15, \"actual burn is smaller than the paid/reported burn\");\n            assertEq(token.balanceOf(collector), collectorBefore, \"paint cost leaked to a spendable fee collector\");\n        } catch {\n            assertEq(canvas.balanceOf(address(this)), 0.99 ether);\n            assertEq(token.balanceOf(address(canvas)), 0.99 ether);\n            assertEq(canvas.totalPixelsPainted(), 0);\n            assertEq(canvas.totalBurned(), 0);\n            assertEq(canvas.pixelAt(0), 0);\n            assertEq(token.balanceOf(canvas.BURN_ADDRESS()), 0);\n            assertEq(token.balanceOf(collector), collectorBefore);\n        }\n    }\n}","reproduction":"Deploy a 1% recipient-deducted fee ERC-20 (18 decimals, fee to a spendable collector) and PixelFun(token, 20). Caller mints 1e18, approves, deposit(1e18): vault credit 0.99e18 (correct). paint([0],[0]). Expected under the brief's burn guarantee: dead address holds PRICE = 5000000000000000 == totalBurned. Actual: totalPixelsPainted = 1, totalBurned = 5000000000000000, Painted.burned = 5000000000000000, but token.balanceOf(0xdEaD) = 4950000000000000 and the collector gained 50000000000000. Ran the attached proof as test/scratch/Proof_6895.t.sol with forge 1.8.3: fails with 'actual burn is smaller than the paid/reported burn: 4950000000000000 != 5000000000000000'. With a no-fee token (the deployed IMD case) the same sequence gives dead balance == totalBurned == 5e15 (my scratch test_packingBoundary).","severity":"low","snippet":"        imd.safeTransfer(BURN_ADDRESS, cost);\n        emit Painted(msg.sender, epoch, ids, colors, cost);","title":"paint() books and emits the nominal cost as burned, but the dead-address transfer is not measured: under a fee-on-transfer token the actual burn is smaller than totalBurned and the Painted.burned fiel"},{"citation":"resolved","description":"Merged from audit_permissions (info) and audit_economics (low); same root cause. PixelFun itself is ownerless (only deposit/withdraw/paint change state, each scoped to msg.sender, no receive/fallback, no delegatecall/selfdestruct). Every vault movement is a token transfer (lines 110, 123, 156), and the asset is not a plain ERC-20. Verified over https://rpc.mainnet.chain.robinhood.com (chain id 4663) during this review: owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7, transfersEnabled() = true, blocked(0xdEaD) = false, no burn(uint256) selector (0x42966c68) in the runtime code, and an eth_call of transfer(0xdEaD, 1) with the storage slot keccak256(abi.encode(0xdEaD, 13)) overridden to 1 reverts 'BridgedFP: blocked', so the blocklist is enforced on live transfers. If the token owner blocks PixelFun's address, withdraw() (line 123), paint() (line 156) and deposit() all revert and every deposited IMD is stuck with no PixelFun-side recovery; if only 0xdEaD is blocked, paint() reverts while withdraw still works. This confirms the author's choice of the dead-address burn (no permissionless burn exists) and the README's deviation note. It is not a PixelFun code defect and cannot be fixed inside it without violating the no-admin brief, but the README trust list ('Timestamps can be nudged') omits it and the brief's 'Always works' guarantee silently rests on it. Fix: document the dependency in README and on the website, and advise users to keep vault balances small (deposit what they will paint).","line":123,"path":"src/PixelFun.sol","reproduction":"Local: ERC-20 mock whose _update reverts 'BridgedFP: blocked' when from or to is blocklisted; PixelFun(token, 20); alice deposit(1 ether); block PixelFun's address; alice withdraw(1) -> reverts 'BridgedFP: blocked' (expected per brief: withdraw of unspent balance always succeeds). Unblock PixelFun, block 0xdEaD: alice paint([0],[0]) reverts 'BridgedFP: blocked', alice withdraw(1) succeeds. Ran as test/scratch/Repro.t.sol::test_blockedPixelFunFreezesWithdraw (passes, i.e. the freeze reproduces). On-chain evidence: cast call TOKEN 'transfer(address,uint256)(bool)' 0xdEaD 1 --from 0x047F...54B7 -> true; same call with --override-state-diff TOKEN:keccak256(abi.encode(0xdEaD,13)):1 -> 'execution reverted: BridgedFP: blocked'; cast code TOKEN contains no 42966c68.","severity":"low","snippet":"        imd.safeTransfer(msg.sender, amount);","title":"Trust assumption not documented: the IMD token has an owner-controlled blocklist and transfer gate, so 'withdraw always works' and painting depend on the token owner, and a block on PixelFun's address"},{"citation":"resolved","description":"From audit_flow. The brief defines PRICE as an immutable computed from decimals() at deployment with a decimals >= 3 check. Because the launch rehearses constructors on an empty chain, the constructor cannot call the token, so the author defers the read and the check to the first deposit()/paint() and caches it; README and launch.json notes document this. Consequence: deployment always succeeds even if imd_ is an EOA, a non-token, or a token with decimals < 3 or > 77; every deposit() and paint() then reverts forever and, the contract being ownerless, nothing can repair it. No funds are at risk (deposit reverts before pulling tokens). The caching is sound: PRICE() returns the live computation until the first state-changing call and the cached value afterwards; 100 * 5 * 10**74 fits uint256, which is why d > 77 is rejected. Kept as informational so the manifest's imd_ argument (0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127, verified decimals() = 18 on chain 4663 during this review) is treated as the one value that must be right before launch.","line":108,"path":"src/PixelFun.sol","reproduction":"new PixelFun(address(tokenWithDecimals2), 20) succeeds; alice deposit(1) reverts UnsupportedDecimals(). new PixelFun(0xBEEF /* EOA */, 20) succeeds; deposit(1) reverts (decimals() returns no data). Expected per the brief: the constructor reverts at deployment. Actual: deployment succeeds and the contract is dead on arrival. Ran as test/scratch/Repro.t.sol::test_lowDecimalsDeploysThenDead.","severity":"info","snippet":"        _price(); // validates the token's decimals once","title":"PRICE is derived from the token's decimals on first deposit/paint instead of in the constructor (documented deviation): a wrong or sub-3-decimal imd_ address deploys successfully and is permanently un"},{"citation":"resolved","description":"From audit_permissions. slotCount is bounded directly but startSlot only through the checked sum startSlot + slotCount, which overflows before the comparison. The call still reverts, no state or funds are involved, and the website only passes startSlot < 8192, so this is cosmetic: the revert reason is not the documented custom error. Minimal fix: check startSlot >= SLOT_COUNT first, or compare startSlot > SLOT_COUNT - slotCount after the slotCount check.","line":278,"path":"src/PixelFun.sol","reproduction":"getPixels(type(uint256).max, 1). Expected: revert BadRange(). Actual: revert Panic(0x11). Ran as test/scratch/Repro.t.sol::test_getPixelsPanicNearMax with vm.expectRevert(PixelFun.BadRange.selector): fails with 'panic: arithmetic underflow or overflow (0x11) != BadRange()'.","severity":"info","snippet":"        if (slotCount == 0 || slotCount > MAX_READ_SLOTS || startSlot + slotCount > SLOT_COUNT) revert BadRange();","title":"getPixels reverts with Panic(0x11) instead of BadRange() when startSlot is near uint256 max"},{"citation":"resolved","description":"From audit_economics. Entry costs MIN_PIXELS * PRICE per address and nothing ties addresses to people, so with MIN_PIXELS = 20 and PRICE = 0.005 IMD an empty board is fully occupied for 66 * 20 * 0.005 = 6.6 IMD of burned tokens; each honest challenger must then strictly exceed the lowest sybil count, and the operator can top that slot up by one pixel. This is exactly what the brief specifies (top 66 painters by pixels painted per address, strict-greater displacement), and the spend is a real burn, so it is not a PixelFun defect; it is recorded for whoever designs the NFT mint that consumes the whitelist. The only in-brief lever is a larger MIN_PIXELS constructor value.","line":154,"path":"src/PixelFun.sol","reproduction":"Fresh deployment, MIN_PIXELS = 20. 66 addresses each deposit 0.1e18 and paint 20 pixels: getBoard(0) returns exactly those 66 addresses, totalBurned = 66 * 20 * 5e15 = 6.6e18. bob deposits and paints 20: isWhitelisted(0, bob) == false (20 not strictly greater than 20). bob paints 1 more: isWhitelisted(0, bob) == true, one sybil displaced. Ran as test/scratch/Repro.t.sol::test_sybilBoard.","severity":"info","snippet":"        if (count >= MIN_PIXELS) _updateBoard(epoch, msg.sender, count);","title":"Whitelist board ranks addresses, so one actor can occupy all 66 slots of an epoch for MIN_PIXELS * PRICE per address (design property of the brief)"}],"hash":"83b10e37af63cbdf9472afa4c788c839e6f081b5adac8f6de55c71ab4cd09824","nodeId":"13b13608-f653-4b63-a514-ce717465be98","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result. No file in the tree was changed.\n\n**Outcome:** PixelFun has no fund-loss defect. All seven specialist findings reproduced, and I merged them into five, two low and three informational. My own pass over the board logic, packed storage, vault accounting, reentrancy guard and deployability found nothing new.\n\n**Findings kept**\n\n- **Low, burn not measured.** The paint function books and emits the nominal cost, then transfers it to the dead address without checking what arrived. Under a fee-on-transfer token the real burn is 1% short of the counter. The tester's proof fails on this code with 4950000000000000 != 5000000000000000, and I attached it. I downgraded it from medium because I confirmed over RPC that the deployed IMD token charges no fee, so the counter is exact on the actual launch. Recommended fix is documentation rather than a balance check, which would brick painting under a fee token.\n- **Low, undocumented trust assumption.** The IMD token is owner-controlled with an enforced blocklist. I verified on chain 4663 that its owner is an EOA, that no burn selector exists, and that overriding the blocklist slot makes a transfer revert with \"BridgedFP: blocked\". Blocking PixelFun's address would freeze every vault balance. The README trust list omits this.\n- **Info, deferred PRICE.** Decimals are read on first use, not in the constructor, so a wrong token address deploys fine and is dead on arrival. Documented deviation forced by the empty-chain constructor rehearsal.\n- **Info, getPixels panics** instead of reverting with BadRange near uint256 max. Cosmetic.\n- **Info, sybil board.** Sixty-six addresses fill the whitelist for 6.6 IMD. This is the brief's design, recorded for the mint designer.\n\n**Coverage:** deposit and withdraw hold, paint carries finding 1, plus four invariant and deployability rows that hold. Existing suite passes, 56 tests. Runtime size is 6,921 bytes and the manifest has only the schema keys with correct argument order.","treeHash":null,"usage":{"cachedInputTokens":1065073,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":20716,"runtime":"claude","turns":17,"wallClockMs":524452}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5c1e4764d4e20ef5","findings":[{"citation":"resolved","description":"Economic-dependency finding (Pashov Economic Security: 'break dependencies'). The live IMD token at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 on Robinhood Chain (chain id 4663, read over https://rpc.mainnet.chain.robinhood.com during this review) is not a plain ERC-20: it is a LayerZero OFT named 'BridgedFP' with owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7 (an EOA), a transfersEnabled() switch (currently true, one-way enableTransfers()), a Uniswap-v4 gate (setV4Config(address,address,bool), 'BridgedFP: not v4 gate'), and a blocked(address) mapping at storage slot 13 that is enforced on BOTH sender and recipient of every transfer ('BridgedFP: blocked'). It exposes no burn(uint256)/burnFrom: burn(1) reverts for every caller, so the contract's choice to transfer to 0xdEaD (line 156) is correct and the README deviation note is confirmed. However, the README's trust list ('Timestamps can be nudged') omits that a single privileged party on the token side can freeze every vault: if the token owner / its gate blocks the PixelFun address, withdraw() (line 123) and paint() (line 156) revert forever and all deposited IMD is stuck; if only 0xdEaD is blocked, paint() reverts while withdraw still works. PixelFun has no admin and no alternative exit, so it cannot mitigate this in code; this is a trust assumption the brief's 'withdraw ... Always works' guarantee silently rests on and that the author should document (and the website should surface), or the author should keep vault balances small by design (e.g. advise users to deposit only what they will paint). No code change is required for the launch; the fix is documentation of the dependency.","line":123,"path":"src/PixelFun.sol","reproduction":"On-chain evidence (eth_call with state override, no transaction sent):\n  cast call 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 'transfer(address,uint256)(bool)' 0x000000000000000000000000000000000000dEaD 1 --from 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7 --rpc-url https://rpc.mainnet.chain.robinhood.com   => true\n  key = keccak256(abi.encode(0x000000000000000000000000000000000000dEaD, 13)); same call with --override-state-diff TOKEN:key:1   => 'execution reverted: BridgedFP: blocked'; the mirror call from 0xdEaD to the owner reverts identically (recipient and sender both checked).\n  cast call TOKEN 'burn(uint256)' 1 --from 0x000000000000000000000000000000000000dEaD => execution reverted (no burn selector 0x42966c68 in the runtime code).\nLocal reproduction (any mock ERC-20 whose _update reverts when from or to is in a blocklist): alice deposit(1 ether); block PixelFun's address; alice withdraw(1) => reverts with the token's blocked error. Expected per brief: withdraw of unspent balance 'always works'. Actual: funds frozen for every depositor until the token side unblocks. With only 0xdEaD blocked: paint([0],[0]) reverts while withdraw succeeds.","severity":"low","snippet":"        imd.safeTransfer(msg.sender, amount);","title":"Vault custody depends on the IMD token's owner-controlled blocklist: 'withdraw always works' cannot hold if PixelFun or 0xdEaD is blocked"},{"citation":"resolved","description":"The board ranks addresses, not people, and entry costs only MIN_PIXELS * PRICE. One actor with 66 addresses can hold the entire whitelist of an epoch for 66 * (max(MIN_PIXELS, currentMin + 1)) * PRICE. With MIN_PIXELS = 20 and PRICE = 0.005 IMD, an empty board is fully occupied for 66 * 20 * 0.005 = 6.6 IMD, and any honest painter must then exceed the lowest sybil count (strictly greater) to enter; the sybil operator can respond by topping up the displaced slot. Because paint() burns the cost, the operator's spend is a real burn, not a refundable deposit, so the economics are bounded and partly serve the token's burn goal. This is the behaviour the brief specifies (top 66 painters by pixels painted per address), so it is reported for the NFT-mint design that consumes the whitelist, not as a defect in PixelFun. If the launch wants it harder, the only in-brief lever is a larger MIN_PIXELS constructor value.","line":154,"path":"src/PixelFun.sol","reproduction":"Fresh deployment, MIN_PIXELS = 20. Actor funds 66 addresses with 0.1 IMD each (deposit(0.1e18) from each) and from each calls paint with 20 ids and colors (e.g. ids 0..19, colors 0). getBoard(0) returns exactly those 66 addresses; total burned 6.6 IMD. Honest painter bob then deposits and paints 20 pixels: isWhitelisted(0, bob) == false (20 is not strictly greater than the minimum 20). Bob paints 1 more (21): bob displaces one sybil. The sybil address paints 2 more (22) and displaces bob again, and so on; the actor's cost to stay ahead is always one pixel above each honest challenger. Expected: as designed. Reported so the mint design accounts for it.","severity":"info","snippet":"        if (count >= MIN_PIXELS) _updateBoard(epoch, msg.sender, count);","title":"Whitelist can be monopolised cheaply with sybil addresses (design property of the per-address top-66 board)"}],"hash":"b2175bb9520d1fb169450f2c67b8f6c3c576aec42414a5146e7cc3d30eee1e78","nodeId":"8ece7f16-6f48-4d4e-b0e6-3b0838f355d0","outcome":"completed","summary":"The review is complete and the findings file is written and valid.\n\n**Outcome:** no exploitable economic or invariant defect in PixelFun itself. The vault, burn and board accounting hold under every attack I traced, including a differential fuzz of the leaderboard against a reference model. Two items are recorded in `.imd-findings.json`:\n\n- **Low, trust assumption on the IMD token.** I reached Robinhood Chain mainnet and inspected the live token. It is an owner-controlled LayerZero OFT named \"BridgedFP\" with a blocklist enforced on both sender and recipient of every transfer. If the token side blocks the PixelFun address, withdraw and paint revert forever and every vault balance is frozen. PixelFun cannot mitigate this in code, but the brief's \"withdraw always works\" guarantee rests on it and the README does not say so. The reproduction is an eth_call with a storage override on the real token that reverts with \"BridgedFP: blocked\".\n- **Info, sybil-able whitelist.** Sixty-six addresses painting the minimum fill an epoch's board for 6.6 IMD burned. That is the behaviour the brief specifies, so it is noted for the NFT-mint design rather than as a defect.\n\n**Verified in the author's favour:** the live token has no burn(uint256) selector and burn reverts for holders, so the transfer to 0xdEaD is the only correct option and the README's deviation note stands. IMD has 18 decimals, so PRICE is exactly 0.005 tokens.\n\n**Coverage:** all three entry points have rows, plus rows for the two brief invariants, the board-equals-top-66 invariant, the burn-method choice, the gas bound and the flow-gap seams. Nothing was left unreached. The differential board test lives under test/scratch and is not part of the submission.\n\nSources used for the RPC endpoint: [Quicknode guide](https://www.quicknode.com/builders-guide/best/top-10-robinhood-chain-rpc-providers), [Dwellir Robinhood Chain docs](https://www.dwellir.com/docs/robinhood).","treeHash":null,"usage":{"cachedInputTokens":1233914,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":30808,"runtime":"claude","turns":30,"wallClockMs":520129}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d11ea2b5e05fa7a8","findings":[{"citation":"resolved","description":"Area: Numerical Gap (precision x invariant). paint() adds `cost` to totalBurned at line 138 (`totalBurned += cost;`) and then transfers `cost` to 0x...dEaD at line 156. The brief's invariant `burned == PRICE * totalPixelsPainted` therefore holds for the counter by construction, but the on-chain burned amount (dead-address balance) is only equal to the counter for a standard ERC-20. With a fee-on-transfer token the dead address receives `cost - fee` while totalBurned and the Painted event's `burned` field report `cost`. The vault itself stays solvent (contract balance == totalDeposited) because the fee is taken out of the transferred amount, so no user funds are at risk; deposit() already handles the fee by crediting the balance difference, so the author clearly considered such tokens, and this is the one remaining place where the fee-token assumption leaks. IMD on Robinhood Chain is a standard bridged ERC-20 with no transfer fee, so this is informational: a token-assumption note for the README (which currently states `totalBurned = PRICE * totalPixelsPainted` as the burn guarantee) rather than a fix. If the author wants the counter to reflect what actually reached the burn address, measure the dead-address balance before and after the transfer, as deposit() does; otherwise document that totalBurned is 'amount spent by painters', which is exact for IMD.","line":156,"path":"src/PixelFun.sol","reproduction":"Deploy PixelFun with a 1% fee-on-transfer ERC-20 (same mock as FeeToken in test/PixelFun.t.sol). Alice deposits 1e18 (credited 0.99e18). Alice calls paint([0],[0]). Expected under the brief's reading: dead address holds PRICE = 5e15 = totalBurned. Actual: totalBurned = 5000000000000000, dead-address balance = 4950000000000000, fee sink receives 50000000000000 from this transfer; totalDeposited == token.balanceOf(PixelFun) still holds. Verified with test/scratch/FeeBurn.t.sol (logs: totalBurned 5000000000000000, dead balance 4950000000000000).","severity":"info","snippet":"        imd.safeTransfer(BURN_ADDRESS, cost);","title":"totalBurned counts the amount sent, not the amount the dead address receives, under a fee-on-transfer token"}],"hash":"d7afd40a9946f379ca163b6c732eba6fcbca63ab8c58b5d6b5d81e38f52a6b18","nodeId":"b7e9fb82-5fad-4b33-8d1a-71252e8f7ab7","outcome":"completed","summary":"The review is complete and the findings file is valid, with the quoted snippet matching the source line.\n\n**Outcome:** the math in PixelFun holds. I found no defect in the Math Precision, Boundary or Numerical Gap areas that affects funds or guarantees, and recorded one informational note.\n\n**What I checked, and how:**\n- **Price arithmetic.** The formula for 0.005 token units was tested at decimals 3 and 77, and a full 100-pixel batch at the top of that range stays far inside uint256. The upper cutoff at 77 decimals is slightly conservative but harmless.\n- **Pixel packing.** A differential fuzz painted random batches biased toward slot edges and the last slot, then compared every painted pixel through both `pixelAt` and the eight-call full-canvas read against a reference map. All matched.\n- **Leaderboard with cached minimum.** A second differential fuzz ran up to 200 paints by 90 painters against a naive full-scan reference of the brief's rules, asserting membership, counts, sort order, tie order and that no outsider exceeds the board minimum. All 256 runs matched. I also reasoned that the board minimum is non-decreasing, which makes the strict-greater displacement rule consistent with the tie rule.\n- **Epoch boundaries.** Behaviour at the last second of an epoch and the first second of the next is consistent across `currentEpoch`, `epochEnded`, `epochStart` and `epochEnd`.\n- **Vault accounting.** No divisions or rounding exist in value paths. Checked subtractions cannot underflow because total deposits always equal the sum of balances.\n\n**The one note, severity info:** under a fee-on-transfer token, the burn counter and the Painted event report the amount sent, while the dead address receives less by the fee. The vault stays solvent. IMD is a standard bridged ERC-20, so this is a documentation point for the README, not a code fix.\n\n**Coverage record:** all three entry points answered, plus six invariant rows. Scratch tests live under test/scratch and are not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":847871,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":22355,"runtime":"claude","turns":17,"wallClockMs":493775}}],"verification":[{"checks":[{"durationMs":2400,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.28s\nCompiler run successful!\n","passed":true},{"durationMs":280,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 21 tests for test/PixelFun.t.sol:PixelFunTest\n[PASS] testFuzz_invariants(uint32,uint8) (runs: 256, μ: 1803019, ~: 1527863)\nLogs:\n  Bound result 17\n\n[PASS] test_batchLimits() (gas: 643238)\n[PASS] test_boardInsertSortedAndDisplace() (gas: 40906964)\n[PASS] test_boundsAndPalette() (gas: 320485)\n[PASS] test_constants() (gas: 45457)\n[PASS] test_constructorZeroAddressReverts() (gas: 3975)\n[PASS] test_depositWithdraw() (gas: 174684)\n[PASS] test_duplicateIdsLastWinsAllPaid() (gas: 223696)\n[PASS] test_epochBoundaries() (gas: 356549)\n[PASS] test_feeOnTransferCreditsReceived() (gas: 925334)\n[PASS] test_frozenBoardAfterEpoch() (gas: 918635)\n[PASS] test_gasFullBatchWithFullBoard() (gas: 39184138)\n[PASS] test_getPixelsRange() (gas: 2420718)\n[PASS] test_insufficientBalance() (gas: 489068)\n[PASS] test_lowDecimalsRejected() (gas: 546334)\n[PASS] test_minPixelsGating() (gas: 445163)\n[PASS] test_packedSlotBoundaries() (gas: 437559)\n[PASS] test_paintCostAndBurn() (gas: 790268)\n[PASS] test_reentrancyBlocked() (gas: 1298770)\n[PASS] test_tieOrderingAndMinUpdateOnIncrease() (gas: 883109)\n[PASS] test_withdrawAfterPaintOnlyUnspent() (gas: 309455)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 135.11ms (248.03ms CPU time)\n\nRan 1 test suite in 136.20ms (135.11ms CPU time): 21 tests passed, 0 failed, 0 skipped (21 total tests)\n","passed":true},{"durationMs":72,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PixelFun.deposit(uint256)\",\"PixelFun.paint(uint32[],uint8[])\",\"PixelFun.withdraw(uint256)\"],\"files\":{\".gitignore\":2,\"README.md\":62,\"foundry.toml\":11,\"launch.json\":13,\"remappings.txt\":2,\"src/PixelFun.sol\":287,\"test/PixelFun.t.sol\":473},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"24e3bf88075e26eb697bad2d70500e6256590334c3cc42c055088ccdceb992d9","verifiedTreeHash":"ff22919362b03396c5f94049b53ce8243cb6c591","verifierVersion":"0.1.0+745c1db4"},{"checks":[{"durationMs":1587,"exitCode":0,"name":"build","output":"Compiling 38 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.47s\nCompiler run successful!\n","passed":true},{"durationMs":182,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 21 tests for test/PixelFun.t.sol:PixelFunTest\n[PASS] testFuzz_invariants(uint32,uint8) (runs: 256, μ: 1803019, ~: 1527863)\nLogs:\n  Bound result 17\n\n[PASS] test_batchLimits() (gas: 643238)\n[PASS] test_boardInsertSortedAndDisplace() (gas: 40906964)\n[PASS] test_boundsAndPalette() (gas: 320485)\n[PASS] test_constants() (gas: 45457)\n[PASS] test_constructorZeroAddressReverts() (gas: 3975)\n[PASS] test_depositWithdraw() (gas: 174684)\n[PASS] test_duplicateIdsLastWinsAllPaid() (gas: 223696)\n[PASS] test_epochBoundaries() (gas: 356549)\n[PASS] test_feeOnTransferCreditsReceived() (gas: 925334)\n[PASS] test_frozenBoardAfterEpoch() (gas: 918635)\n[PASS] test_gasFullBatchWithFullBoard() (gas: 39184138)\n[PASS] test_getPixelsRange() (gas: 2420718)\n[PASS] test_insufficientBalance() (gas: 489068)\n[PASS] test_lowDecimalsRejected() (gas: 546334)\n[PASS] test_minPixelsGating() (gas: 445163)\n[PASS] test_packedSlotBoundaries() (gas: 437559)\n[PASS] test_paintCostAndBurn() (gas: 790268)\n[PASS] test_reentrancyBlocked() (gas: 1298770)\n[PASS] test_tieOrderingAndMinUpdateOnIncrease() (gas: 883109)\n[PASS] test_withdrawAfterPaintOnlyUnspent() (gas: 309455)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 69.70ms (137.72ms CPU time)\n\nRan 1 test suite in 73.33ms (69.70ms CPU time): 21 tests passed, 0 failed, 0 skipped (21 total tests)\n","passed":true},{"durationMs":42,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PixelFun.deposit(uint256)\",\"PixelFun.paint(uint32[],uint8[])\",\"PixelFun.withdraw(uint256)\"],\"files\":{\".gitignore\":2,\"README.md\":62,\"foundry.toml\":11,\"remappings.txt\":2,\"src/PixelFun.sol\":287,\"test/PixelFun.t.sol\":473},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":939,"exitCode":0,"name":"slither","output":"[low/medium] timestamp at src/PixelFun.sol:237: PixelFun.epochEnded(uint256) (src/PixelFun.sol#237-239) uses timestamp for comparisons","passed":true},{"durationMs":298,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/PixelFun.sol:109: Reentrancy: State change after external call (2 places)\n[low] costly-loop at src/PixelFun.sol:140: Costly operations inside loop\n[low] literal-instead-of-constant at src/PixelFun.sol:93: Literal Instead of Constant (15 places)\n[low] require-revert-in-loop at src/PixelFun.sol:140: Loop Contains `require`/`revert`\n[low] unchecked-return at src/PixelFun.sol:108: Unchecked Return\n[low] uninitialized-local-variable at src/PixelFun.sol:140: Uninitialized Local Variable (2 places)\n[low] unused-public-function at src/PixelFun.sol:86: Public Function Not Used Internally (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"64c11b64ef55594d4e4ad72c23886bd7b9b65b26f56b5707fad5597708f7b520","verifiedTreeHash":"209515a334b11161e3195bbdb1968e5d24266367","verifierVersion":"0.1.0+745c1db4"},{"checks":[{"durationMs":3308,"exitCode":0,"name":"build","output":"Compiling 45 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.22s\nCompiler run successful!\n","passed":true},{"durationMs":102238,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/PixelFunBoardProperties.t.sol:PixelFunBoardPropertiesTest\n[PASS] testFuzz_MinPixelsIsInclusiveAndCumulative(uint16) (runs: 1000, μ: 1112100, ~: 944272)\nLogs:\n  Bound result 22\n\n[PASS] test_EpochCutoffFreezesFullBoardAndGatingRestartsFromZero() (gas: 34203229)\n[PASS] test_FailedDisplacingBurnPreservesIncumbentAndMembershipIndexes() (gas: 33107881)\n[PASS] test_IncreasingCachedMinimumChangesWhoIsDisplaced() (gas: 33143655)\n[PASS] test_SameBlockTiesPreferFirstReachNotFirstAdmission() (gas: 1873489)\n[PASS] test_StrictCutoffLatestTieEvictionAndDisplacedPainterReentry() (gas: 34166641)\n[PASS] test_ZeroMinimumStillRequiresPaintingAndNeverDuplicatesEntry() (gas: 821674)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 213.20ms (282.62ms CPU time)\n\nRan 21 tests for test/PixelFun.t.sol:PixelFunTest\n[PASS] testFuzz_invariants(uint32,uint8) (runs: 256, μ: 1894958, ~: 1681172)\nLogs:\n  Bound result 13\n\n[PASS] test_batchLimits() (gas: 643238)\n[PASS] test_boardInsertSortedAndDisplace() (gas: 40906964)\n[PASS] test_boundsAndPalette() (gas: 320485)\n[PASS] test_constants() (gas: 45457)\n[PASS] test_constructorZeroAddressReverts() (gas: 3975)\n[PASS] test_depositWithdraw() (gas: 174684)\n[PASS] test_duplicateIdsLastWinsAllPaid() (gas: 223696)\n[PASS] test_epochBoundaries() (gas: 356549)\n[PASS] test_feeOnTransferCreditsReceived() (gas: 925334)\n[PASS] test_frozenBoardAfterEpoch() (gas: 918635)\n[PASS] test_gasFullBatchWithFullBoard() (gas: 39184138)\n[PASS] test_getPixelsRange() (gas: 2420718)\n[PASS] test_insufficientBalance() (gas: 489068)\n[PASS] test_lowDecimalsRejected() (gas: 546334)\n[PASS] test_minPixelsGating() (gas: 445163)\n[PASS] test_packedSlotBoundaries() (gas: 437559)\n[PASS] test_paintCostAndBurn() (gas: 790268)\n[PASS] test_reentrancyBlocked() (gas: 1298770)\n[PASS] test_tieOrderingAndMinUpdateOnIncrease() (gas: 883109)\n[PASS] test_withdrawAfterPaintOnlyUnspent() (gas: 309455)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 215.74ms (300.13ms CPU time)\n\nRan 16 tests for test/PixelFunAccounting.t.sol:PixelFunAccountingTest\n[PASS] testFuzz_DecimalScalingPaysExactPrice(uint8,uint8) (runs: 1000, μ: 1277746, ~: 1207810)\nLogs:\n  Bound result 11\n  Bound result 43\n\n[PASS] testFuzz_DepositPaintWithdrawConservesEachUsersFunds(uint128,uint8) (runs: 1000, μ: 874230, ~: 803926)\nLogs:\n  Bound result 25\n  Bound result 340282366920938463463354607431768211455\n\n[PASS] testFuzz_FeeDepositWithdrawRoundTrip(uint128,uint16) (runs: 1000, μ: 1142581, ~: 1145045)\nLogs:\n  Bound result 105000000000000000\n  Bound result 3920\n\n[PASS] test_DepositRejectsAllThreeReentrantEntryPoints() (gas: 5092119)\n[PASS] test_DepositZeroAndMissingAllowanceLeaveNoLiability() (gas: 318384)\n[PASS] test_DonationCannotBeWithdrawnOrCreditedByNextDepositor() (gas: 484431)\n[PASS] test_FailedBurnRollsBackCanvasCountersVaultAndBoard() (gas: 6840233)\n[PASS] test_FailedDepositRollsBackAndCanBeRetried() (gas: 433016)\n[PASS] test_FailedWithdrawalPreservesFundsAndOtherUsersCanExit() (gas: 743168)\n[PASS] test_MsgSenderOwnsVaultRegardlessOfTxOrigin() (gas: 344036)\n[PASS] test_NoReturnTokenSupportsEntireLifecycle() (gas: 904036)\n[PASS] test_OneWeiCanBeDepositedAndFullyWithdrawn() (gas: 347398)\n[PASS] test_PaintRejectsAllThreeReentrantEntryPoints() (gas: 5699420)\n[PASS] test_RevertingTokenBubblesAndAccountingRollsBack() (gas: 463126)\n[PASS] test_UnsupportedLowDecimalsNeverAcceptFunds() (gas: 2674462)\n[PASS] test_WithdrawRejectsAllThreeReentrantEntryPoints() (gas: 5012621)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 220.07ms (628.22ms CPU time)\n\nRan 8 tests for test/PixelFunCanvasProperties.t.sol:PixelFunCanvasPropertiesTest\n[PASS] testFuzz_BatchPartitionPreservesCanvasAndPayment(bytes32,uint8,uint8) (runs: 1000, μ: 1069032, ~: 933728)\nLogs:\n  Bound result 34\n  Bound result 27\n\n[PASS] testFuzz_EpochViewsAtExactEdges(uint16) (runs: 1000, μ: 87010, ~: 86891)\nLogs:\n  Bound result 101\n\n[PASS] testFuzz_InvalidItemAtAnyPositionRevertsEntireBatch(uint8,uint8,uint32,uint8,bool) (runs: 1000, μ: 244608, ~: 232697)\nLogs:\n  Bound result 53\n  Bound result 25\n  Bound result 32\n\n[PASS] testFuzz_OverwritesMatchByteArrayAcrossArbitrarySlotBoundary(uint16,uint8,bytes32) (runs: 1000, μ: 1490455, ~: 1454758)\nLogs:\n  Bound result 37\n  Bound result 1\n\n[PASS] test_AllPaletteValuesAndRowBoundaryAreEncodedAsIndexPlusOne() (gas: 1123395)\n[PASS] test_FreshCanvasIsUnpaintedIncludingLastPage() (gas: 19820450)\n[PASS] test_OneWeiShortCannotPaintAndExactFundingCan() (gas: 426655)\n[PASS] test_ReadRangeEdgesRejectWithoutAffectingCanvas() (gas: 2536733)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 220.15ms (682.62ms CPU time)\n\nRan 1 test for test/PixelFunInvariant.t.sol:PixelFunFeeVaultInvariantTest\n[PASS] invariant_FeeVaultRemainsSolventAcrossDepositsWithdrawalsAndDonations() (runs: 256, calls: 24576, reverts: 0)\n\n╭-----------------+--------------------+-------+---------+----------╮\n| Contract        | Selector           | Calls | Reverts | Discards |\n+===================================================================+\n| PixelFunHandler | advanceTime        | 3541  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | deposit            | 3533  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | donate             | 3511  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | rejectOverspending | 3472  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | roundTrip          | 3455  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | withdraw           | 3515  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | withdrawAll        | 3549  | 0       | 0        |\n╰-----------------+--------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2243\n  Bound result 6\n  Bound result 77\n  Bound result 94086\n  Bound result 295\n  Bound result 10365\n  Bound result 1\n  Bound result 64206\n  Bound result 5833\n  Bound result 85\n  Bound result 3327641368\n  Bound result 38664\n  Bound result 20\n  Bound result 43158465327056612527\n  Bound result 65536\n  Bound result 1025\n  Bound result 1106960347605753917\n  Bound result 5260\n  Bound result 40\n  Bound result 3000000000000000000\n  Bound result 1024\n  Bound result 2925999104286633230\n  Bound result 1264\n  Bound result 1312229131\n  Bound result 471\n  Bound result 1025\n  Bound result 4400830291964165936\n  Bound result 5000000000000000\n  Bound result 3463\n  Bound result 1259200\n  Bound result 33\n  Bound result 5000000000000000000\n  Bound result 192\n  Bound result 2000000000000000000\n  Bound result 64206\n  Bound result 1\n  Bound result 636\n  Bound result 1\n  Bound result 1\n  Bound result 8\n  Bound result 1554\n  Bound result 2000000000000000000\n  Bound result 9624522866994574533\n  Bound result 1980\n  Bound result 262080\n  Bound result 250\n  Bound result 6968\n  Bound result 43158465327056612527\n  Bound result 1787\n  Bound result 8319294865661418010\n  Bound result 71376994840080832015\n  Bound result 160\n  Bound result 6839\n  Bound result 6869001904010415\n  Bound result 160\n  Bound result 20\n  Bound result 2420\n  Bound result 65\n  Bound result 3000000000000000000\n  Bound result 31\n  Bound result 176\n  Bound result 10000\n  Bound result 7168\n  Bound result 105000000000000000\n  Bound result 19\n  Bound result 5000\n  Bound result 8413\n  Bound result 53158465327056618433\n  Bound result 1510\n  Bound result 6566\n  Bound result 1\n  Bound result 52295227204592166628\n  Bound result 8190\n  Bound result 168274\n  Bound result 1\n  Bound result 18625703646437923970\n  Bound result 2878\n  Bound result 50\n  Bound result 153967104919495106\n  Bound result 4652\n  Bound result 1\n  Bound result 3213\n  Bound result 142312\n  Bound result 1106960347605754942\n  Bound result 229598\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.48s (6.47s CPU time)\n\nRan 2 tests for test/PixelFunInvariant.t.sol:PixelFunInvariantTest\n[PASS]\nPixelFunInvariantTest invariants:\n[PASS] invariant_CanvasMatchesLastWritesAndFinishedBoardsStayFrozen\n[PASS] invariant_VaultSolvencyAndActualBurnsMatchIndependentAccounting\n PixelFunInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭-----------------+--------------------+-------+---------+----------╮\n| Contract        | Selector           | Calls | Reverts | Discards |\n+===================================================================+\n| PixelFunHandler | advanceTime        | 3174  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | deposit            | 2985  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | donate             | 3123  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | paint              | 3077  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | rejectOverspending | 2974  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | roundTrip          | 3181  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | withdraw           | 3011  | 0       | 0        |\n|-----------------+--------------------+-------+---------+----------|\n| PixelFunHandler | withdrawAll        | 3051  | 0       | 0        |\n╰-----------------+--------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 551\n  Bound result 33\n  Bound result 703\n  Bound result 434223404912192741\n  Bound result 698\n  Bound result 926\n  Bound result 26\n  Bound result 63374607431768211455\n  Bound result 15\n  Bound result 9334\n  Bound result 2073600\n  Bound result 12409\n  Bound result 3387\n  Bound result 731\n  Bound result 3514649357\n  Bound result 11273\n  Bound result 659918\n  Bound result 96\n  Bound result 13842\n  Bound result 486\n  Bound result 100\n  Bound result 8127\n  Bound result 187747980899\n  Bound result 8653417951212149884\n  Bound result 18\n  Bound result 2077221489526167103\n  Bound result 1\n  Bound result 173401\n  Bound result 64206\n  Bound result 231985\n  Bound result 11698\n  Bound result 101\n  Bound result 5417\n  Bound result 65\n  Bound result 19\n  Bound result 163907\n  Bound result 13150\n  Bound result 513462039\n  Bound result 847\n  Bound result 30\n  Bound result 18\n  Bound result 15194\n  Bound result 52\n  Bound result 5985\n  Bound result 88374607431768223143\n  Bound result 29\n  Bound result 485951957\n  Bound result 1\n  Bound result 105000000000000000\n  Bound result 1\n  Bound result 251\n  Bound result 5\n  Bound result 1\n  Bound result 6\n  Bound result 23133693297245516500\n  Bound result 49861044779750720843\n  Bound result 11200\n  Bound result 39217276388\n  Bound result 2\n  Bound result 52784929640080264574\n  Bound result 2073600\n  Bound result 40\n  Bound result 7\n  Bound result 5760\n  Bound result 92\n  Bound result 44818219288653197812\n  Bound result 2342098027117572848\n  Bound result 2003016792875641017\n  Bound result 11947\n  Bound result 1662626213188464936\n  Bound result 100000000000000000000\n  Bound result 14966\n  Bound result 1\n  Bound result 5636\n  Bound result 1259200\n  Bound result 4339870967846790768\n  Bound result 259199\n  Bound result 2000000000000000000\n  Bound result 48\n  Bound result 6933\n  Bound result 6749215051284431314\n  Bound result 85\n  Bound result 2878\n  Bound result 3726\n  Bound result 745\n  Bound result 9812952612669230127\n  Bound result 594\n\n[PASS] test_HandlerExercisesEveryOperationAndClosesAllPositions() (gas: 4712680)\nLogs:\n  Bound result 1\n  Bound result 100\n  Bound result 1000000000000000000\n  Bound result 1\n  Bound result 1\n  Bound result 1\n  Bound result 259200\n  Bound result 20\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 18.21s (18.21s CPU time)\n\nRan 1 test for test/PixelFunBoardProperties.t.sol:PixelFunBoardInvariantTest\n[PASS] invariant_BoardContainsExactlyTheTop66ByScoreAndReachOrder() (runs: 256, calls: 24576, reverts: 0)\n\n╭----------------------+-------------------------+-------+---------+----------╮\n| Contract             | Selector                | Calls | Reverts | Discards |\n+=============================================================================+\n| PixelFunBoardHandler | advanceWithinEpoch      | 6171  | 0       | 0        |\n|----------------------+-------------------------+-------+---------+----------|\n| PixelFunBoardHandler | catchUpToAnotherPainter | 6172  | 0       | 0        |\n|----------------------+-------------------------+-------+---------+----------|\n| PixelFunBoardHandler | increaseCurrentMinimum  | 6138  | 0       | 0        |\n|----------------------+-------------------------+-------+---------+----------|\n| PixelFunBoardHandler | paint                   | 6095  | 0       | 0        |\n╰----------------------+-------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 16\n  Bound result 100\n  Bound result 1159\n  Bound result 77\n  Bound result 27\n  Bound result 2297\n  Bound result 645\n  Bound result 48\n  Bound result 90\n  Bound result 17\n  Bound result 38\n  Bound result 69\n  Bound result 498\n  Bound result 935\n  Bound result 70\n  Bound result 7\n  Bound result 99\n  Bound result 2961\n  Bound result 41\n  Bound result 100\n  Bound result 100\n  Bound result 46\n  Bound result 67\n  Bound result 84\n  Bound result 18\n  Bound result 1093\n  Bound result 16\n  Bound result 18\n  Bound result 3262\n  Bound result 11\n  Bound result 30\n  Bound result 96\n  Bound result 98\n  Bound result 80\n  Bound result 4\n  Bound result 1415\n  Bound result 100\n  Bound result 4\n  Bound result 41\n  Bound result 73\n  Bound result 3351\n  Bound result 99\n  Bound result 1322\n  Bound result 99\n  Bound result 31\n  Bound result 93\n  Bound result 96\n  Bound result 2\n  Bound result 2989\n  Bound result 160\n  Bound result 100\n  Bound result 100\n  Bound result 2989\n  Bound result 99\n  Bound result 70\n  Bound result 100\n  Bound result 1340\n  Bound result 3530\n  Bound result 99\n  Bound result 7\n  Bound result 37\n  Bound result 95\n  Bound result 3014\n  Bound result 64\n  Bound result 39\n  Bound result 2\n  Bound result 3600\n  Bound result 100\n  Bound result 137\n  Bound result 100\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 102.15s (102.12s CPU time)\n\nRan 7 test suites in 102.15s (127.71s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":39,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PixelFun.deposit(uint256)\",\"PixelFun.paint(uint32[],uint8[])\",\"PixelFun.withdraw(uint256)\"],\"files\":{\".gitignore\":2,\"README.md\":62,\"foundry.toml\":11,\"remappings.txt\":2,\"src/PixelFun.sol\":287,\"test/PixelFun.t.sol\":473,\"test/PixelFunAccounting.t.sol\":333,\"test/PixelFunBoardProperties.t.sol\":243,\"test/PixelFunCanvasProperties.t.sol\":222,\"test/PixelFunInvariant.t.sol\":158,\"test/README.md\":30,\"test/helpers/PixelFunBoardHandler.sol\":76,\"test/helpers/PixelFunHandler.sol\":184,\"test/helpers/PixelFunTokens.sol\":121},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7a47744b56755cfffb7cb620177b6684a8be33c6323af7de2bd86bfc5aff31ef","verifiedTreeHash":"432ceb8eb9f50ae44a774d8a517b76d5e1294bf5","verifierVersion":"0.1.0+745c1db4"}]}