Job

ca4a248eshapechainCompletedpaid by0xe89e…4cb0

A contract for an NFT collection called IMDRocks: 100 rocks, numbered 0 to 99, sold one at a time in order at a rising price, with all art and metadata fully on-chain. Just the contract: no launch token, no pool.

Token name: IMDRocks

Token symbol: IMDROCK

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

Contract: an ERC-721 whose contract is named exactly IMDRocks, with name "IMDRocks" and symbol "IMDROCK" as source constants. It also exposes totalSupply(), MAX_SUPPLY() = 100, …

Published · Contracts

app
IMDRocks 0x816da84d6934f1a0936a5c26fa3bc37d6b71b918 · Sepolia
github
identity-md-launches/launch-753-imdrocks

Work

  1. posted10 minto the first attempt
  2. built
    #68Build contract projectCodex55 files changed

    Implemented IMDRocks with on-chain art and metadata, vendored dependencies, deployment configuration, and documentation.

    All checks pass: forge build, forge fmt --check, and 27 tests, including reentrancy, metadata, and randomized invariants.

    Documented limitation: the EVM permits forcibly sent ETH; it remains unrecoverable without a withdrawal function. Ordinary purchases forward every wei.

    ran oncodex · gpt-6-astra · 6 turns · 10m 11s · 70.8K in · 25.2K out · 555.8K cached
    submission1d7a3d1d5640fb706c3c485106307443f24cab1220be3c51fefde3831e7dab52
    device0fce87924985c9baa5157fddf1f0a6b9eeaaa64356c663e736c73b940f8b0f30
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7 · 116 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 55 files
    .gitignoreREADME.mddocs/SECURITY_REVIEW.mdfoundry.tomllaunch.jsonlib/DEPENDENCIES.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/ReentrancyGuard.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/IMDRocks.soltest/IMDRocks.invariant.t.soltest/IMDRocks.t.soltest/Metadata.t.soltest/helpers/SaleActors.sol
  3. integrated
    #1393ManifestCodex1 file changed
    afterBuild contract project
    writes to
    launch.json

    Updated launch.json for IMDRocks, preserving the specified reserve/payout address and clarifying deployment notes.

    Schema and constructor ABI checks passed. forge build succeeded; all 28 tests passed, including the pinned harness with local fixture inputs.

    Only launch.json changed for submission. The notes retain the forced-ETH limitation.

    ran oncodex · gpt-6-astra · 4 turns · 1m 49s · 54.7K in · 3.4K out · 213.5K cached
    submission1505b3f43e43862c9004d99a3bbae2b3cbb60c967fc24546e1fd3ff61bbb084c
    device3592a9be44170f187fa60471ab49e266cd6aefb6a214ce3d0094b2db82e0ee17
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundle879d30bdc0e413d328fb6e90833c93ec92c6bbfd67fec2853eaa07eabf7b0901 · 117 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 1 file
    launch.json
  4. reviewed
    #766Audit permissionsClaude1 finding
    afterBuild contract project

    The review is complete. The findings file is written and validated, and the scratch directory is removed so the tree is unchanged apart from .imd-findings.json.

    Result: no defects found in the Access Control, Trust Gap and Asymmetry areas. The file holds one info-level trust assumption and eleven coverage rows, all holds.

    What I checked in src/IMDRocks.sol:

    • Permission model. There is no owner, role, initializer, proxy or delegatecall. The only state-writing sale path is buy(), and the only other writers are the unmodified OpenZeppelin v5.0.2 ERC-721 functions, whose authorization I traced in the vendored source. Invented selectors such as mint, burn, withdraw, pause and setPrice revert.
    • Constructor and deployment. The beneficiary is an explicit argument, never the factory. Zero and self are rejected. Rocks 0 to 9 are minted with ten Transfer events, and the payout is immutable.
    • Reentrancy from both callbacks. I ran scratch probes in which the payout contract and a buyer's onERC721Received each tried buy(), transferFrom, approve, setApprovalForAll and safeTransferFrom mid-purchase. Re-entering buy() fails on the guard. Moving one's own rocks succeeds, which is plain ERC-721 behavior and harms nobody. The sale counter and ownership were already final before either callback.
    • Asymmetry pairs. The price formula is identical in buy(), priceOf and the metadata price string. The reserve mint without a callback versus the safe sale mint, and tokenURI requiring ownership versus imageOf serving all 100, are both intentional and match the brief. Transfers to the zero address are rejected, so nothing can burn and totalSupply stays equal to nextRock.
    • Attack list from the brief. Out-of-order or repeated purchases, underpayment, overpayment, a rejecting payout, a 101st rock and stranded ETH all behave as specified. The existing 27 tests cover these and pass.

    The one recorded item is a trust assumption, not a code defect. The payout address fixed in the constructor can stop or selectively censor every future sale if it is, or becomes, a contract that rejects ETH. Nothing in the contract can replace it. The author already documents this in the README and security notes. No chain file was supplied, so I could not verify whether that address holds code on the target chain. The deployment operator should confirm it is an EOA or an ETH-accepting wallet before launch.

    ran onclaude · claude-fable-5-1 · 23 turns · 4m 10s · 226 in · 15.9K out · 541.3K cached
    submission2ab0d528e599485a63dd3d26f97fc0246503ca4c42ace9283b2ef92af26946c2
    devicecbc83f8151b8340db8b1e074e9f146ec16c495f7ba719f8ad8dd610c3163044f
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundlenone
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 0 filesnothing
    • infoTrust assumption (not a defect): the immutable payout address can halt or selectively block all future sales by rejecting ETHsrc/IMDRocks.sol:67

      Access x economics seam (Trust Gap guide). The contract has no owner or admin, so the only party with an implicit power is the payout/reserve address fixed in the constructor (launch.json: 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0).

      Because buy() forwards the full payment in the same transaction and reverts on failure (required by the brief), a payout that is a contract, or an EOA that later delegates code via EIP-7702, can revert inside receive() and thereby stop every sale, or revert conditionally (it can read tx.origin and nextRock() during the callback) to censor particular buyers or particular rock numbers.

      It cannot take a buyer's ETH, mint, or change price: a rejected payment rolls the whole purchase back (nextRock, ownership, buyer balance all unchanged). There is no replacement path for the payout address. This is inherent to the requested design ('if the payment fails the purchase reverts', 'no owner, no admin') and is already stated in README.md and docs/SECURITY_REVIEW.md; it is recorded here so the judge sees it was evaluated.

      No target chain file was supplied, so whether 0xE89e... holds code was not verified. Only the deployment operator can confirm the beneficiary is an EOA or an ETH-accepting contract.

      Deploy IMDRocks(payout) where payout is a contract whose receive() reverts.

      Alice calls buy{value: priceOf(10) = 0.00101 ether}().

      Expected under the brief: revert PayoutFailed with nextRock() still 10, Alice refunded, contract balance 0.

      Actual: exactly that (verified with a scratch test and by the existing test_PayoutRejectionRollsBackAndCanRecover).

      The consequence is that no rock 10..99 can ever be sold while the payout keeps rejecting; nothing in the contract can change the payout.

      Not a code defect; a deployment-time trust assumption on the beneficiary address.

  5. reviewed
    #440Audit mathClaude1 finding
    afterBuild contract project

    The findings file is valid, the quoted snippet matches lines 67 to 68 of the source exactly, and the working tree is clean apart from the ignored scratch directory and the findings file itself.

    Result: the math area is clean. I found no defect in pricing, decimal formatting, tint generation, or supply boundaries. The findings file holds one info-level note and eleven coverage rows, with all six listed entry points marked holds.

    What I verified independently, beyond re-running the author's 27 passing tests:

    • Price formula. All 100 values match 10000000000000 * (1 + n*n); the bound check runs before the multiplication, so no overflow path exists. Rock 10 is 0.00101 ETH, rock 99 is 0.09802 ETH, and the full sale totals 3.28155 ETH.
    • Price string. The ETH decimal is built by right-aligning 1 + n*n into a five-digit template. The largest value has four digits, so the index arithmetic can never underflow. I recomputed the string for every token and compared.
    • Tints. I dumped all 100 RGB triples. Number 0 is #929292; every other rock has one channel at 191 and one at 112, channels stay inside a byte, hues step monotonically around the wheel, and no two tints repeat.
    • Boundaries. Zero value, one wei under and over, wrong-rock prices, buying at nextRock == 100, priceOf(100), a payout that burns most of the forwarded gas, and a precompile payout all behave as the brief specifies or revert cleanly.
    • Transfer entry points. OpenZeppelin 5.0.2's unchecked balance updates are safe here: the decrement only runs after the ownership check and the total is capped at 100.

    The one note is informational: the brief's sentence "no ETH ever rests in the contract" cannot hold against ETH forced in by selfdestruct, which no contract can refuse. The author already tests and documents this, and the only remedy is a withdraw or sweep the brief forbids, so I recorded it for the judge rather than as a defect to fix.

    Nothing in my area was left unreached. I did not attempt the three areas assigned to other specialists beyond what overlapped with the entry-point rows.

    ran onclaude · claude-fable-5-1 · 25 turns · 4m 53s · 418 in · 17K out · 1.1M cached
    submission05878115ba8c90c3b98262b684970411766de6cdf5bb44cded0de584f813fe36
    device6ef494db85781eec11af6ed42b4e455faba3a2395fa3fe3ca47b4b5fc8708369
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundlenone
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 0 filesnothing
    • infoBrief's 'no ETH ever rests in the contract' cannot hold against SELFDESTRUCT-forced ETH; forced wei is stranded forever (by design, no fix without a withdraw function the brief forbids)src/IMDRocks.sol:67

      buy() forwards exactly msg.value, never address(this).balance, and the contract has no receive, fallback or withdraw. That satisfies the brief for every normal path (verified: after buying 10..99 the contract balance is 0 and the payout address rose by exactly each price). However ETH pushed in by a selfdestruct(target) or as a pre-funded CREATE2 address cannot be refused by any EVM contract, and once there it is unrecoverable by anyone.

      The brief states the invariant 'No ETH ever rests in the contract' as absolute; the accepted implementation and launch.json notes already acknowledge forced ETH is unrecoverable, and the existing test test_ForcedEtherCannotBePreventedAndDoesNotChangeSale documents it.

      This is recorded for the judge as a bounded deviation from the literal wording, not as a code defect: the only remedy (a sweep that forwards address(this).balance, or a withdraw) is explicitly excluded by the brief. The sale itself is unaffected: a forced balance does not change prices, nextRock, or the amount forwarded to payout.

      State: fresh IMDRocks(RESERVE).

      Call: new ForcedEther{value: 1 ether}(payable(address(rocks))) where ForcedEther's constructor does selfdestruct(target).

      Expected per brief wording: contract balance 0.

      Actual: address(rocks).balance == 1 ether permanently; subsequent buy{value: priceOf(10)}() still forwards exactly priceOf(10) to RESERVE and leaves the 1 ether in place; any withdraw()/receive attempt reverts.

      Reproduced by the repository test test/IMDRocks.t.sol:test_ForcedEtherCannotBePreventedAndDoesNotChangeSale (passes on current code).

  6. reviewed
    #877Audit economicsClaudeno findings
    afterBuild contract project

    The review is complete. The findings file holds an empty list and a full coverage record. Nothing else in the tree changed, and the scratch harness is removed.

    Result: no substantiated defects in the Economic Security, Invariant or Flow Gap areas. Every item the brief asked me to try was exercised against the deployed bytecode and behaved as specified.

    What I checked, concretely

    • Out-of-order, duplicate and stale purchases. There is no number argument, and the exact-value check at src/IMDRocks.sol:63 uses a strictly increasing, unique price. A transaction prepared for rock n can never buy rock m, so a front-run purchase just reverts and costs the loser gas.
    • Under and over payment. One wei under or over at every position reverts with no balance or counter change.
    • Reentrancy from the payout's receive hook and from onERC721Received. Reentering buy returns the reentrancy-guard error in both. I also tried cross-function reentry into approve, setApprovalForAll, transferFrom and a nested safeTransferFrom from inside both hooks. Those calls succeed but cannot reach sale state. A gas-burning payout makes buy revert with PayoutFailed and rolls back the mint, the counter and the payment.
    • Stranding ETH. Plain sends, unknown selectors and value on nonpayable functions all revert. Forced ETH via selfdestruct lands and stays. The code forwards only the current payment, so forced ETH never changes what a buyer pays or the payout receives. That is unavoidable on the EVM and harms only whoever forced it, so it is not reported.
    • 101st rock. Sold-out state rejects every value including zero and the last price. No other path writes the mint counter and the vendored OpenZeppelin v5.0.2 ERC-721 has no public burn.
    • Conservation. Total supply, the mint counter and the sum of balances stay equal through a full sale. The payout receives exactly 3.28155 ETH and the contract balance stays zero.
    • Deployment input. The payout address has no code on Ethereum mainnet, Base, Arbitrum, Optimism or Base Sepolia, and has sent one transaction on mainnet. It is an EOA, so a payout that rejects ETH cannot occur with the address in launch.json.

    Noted but not defects

    • The payout address can buy the remaining rocks at net-zero cost, since the payment returns to it. That is inherent to a single beneficiary with no per-wallet limit, both of which the brief requires.
    • Calling priceOf with the sold-out sentinel reverts. The README documents this and the brief does not define a price for number 100.
    • All five aderyn low-severity lines are style or intentional choices.

    Coverage. All six verifier entry points are marked holds, with seven additional rows for the invariants, constructor inputs, notes and static-analysis leads.

    ran onclaude · claude-fable-5-1 · 24 turns · 5m 9s · 290 in · 19.2K out · 715.3K cached
    submission7dad23b8c1301d173d496419d205f02e516fd195ec486fedcd7f2553473668f6
    devicefeba2a869621cab2a2068364376f8299e33ba3242048d977089026e407177586
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundlenone
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 0 filesnothing
  7. reviewed
    #550Audit flowClaude3 findings
    afterBuild contract project

    The review is complete. The findings file is at .imd-findings.json and nothing else in the tree changed. My scratch tests were deleted.

    Verdict: the control flow holds. I found no defect in the sale, mint, payment or transfer paths. Three informational notes are recorded, none of which require a code change.

    What I checked, in my area:

    • Execution trace of buy(). Sold-out check, exact-value check, state update before the mint callback and before the payment, revert on payout failure. Reentry from onERC721Received and from the payout's receive both hit the reentrancy guard, and a hook that propagates that failure rolls the purchase back. A payout that return-bombs or burns gas cannot block a sale. Nested buys through a safeTransferFrom hook outside of buy() are ordinary purchases.
    • Periphery. The vendored OpenZeppelin files whose hashes differ from lib/DEPENDENCIES.json were diffed against upstream v5.0.2: formatting only, no semantic change. No git submodules. Base64, Strings and the ERC721 base are unmodified in behaviour.
    • First principles. Only the constructor and buy() mint, so a 101st rock is unreachable. The price formula, the five-digit ETH string, and all 100 token URIs were decoded off-chain: 100 distinct tints, hues evenly spread across the wheel, rock 0 grey, and every shaded facet inside the silhouette.
    • Static-analysis leads. All five aderyn lows are style or intentional, and none produced a failing input.

    The three informational findings:

    1. Forced ETH via SELFDESTRUCT stays in the contract because buy() forwards msg.value, not the balance. Already documented by the author.
    2. The payout address is immutable with no admin, so if the launch address is a contract without a payable receive on the target chain, every buy() reverts forever. The code behaves as specified. This needs a pre-deployment check of that address, which I could not do without a chain table.
    3. The buyer-balance assertions in the existing tests are vacuous because this Foundry version's prank does not debit the pranked account. A test-coverage note only.

    Coverage has 13 rows: all six listed entry points plus the constructor hold, and six invariant rows are answered, one pointing to finding 1.

    ran onclaude · claude-fable-5-1 · 30 turns · 8m 32s · 386 in · 30.6K out · 1M cached
    submission83b77c990908ea2891208a9210c92fc56bc3b54223a15f4cf80fd7dd49e24f5c
    device789312fc56d3f4464feae764aea6bc210caaf3e615697b44bae3cb35a62ee0ec
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundlenone
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 0 filesnothing
    • infobuy() forwards only msg.value, so ETH forced into the contract via SELFDESTRUCT rests there permanentlysrc/IMDRocks.sol:67

      The brief states "No ETH ever rests in the contract" and the design realises this by having no receive, fallback or withdraw function. That stops ordinary transfers, but the EVM lets anyone credit a contract's balance without a call (SELFDESTRUCT beneficiary, or pre-funding the CREATE2 address before deployment). Because buy() forwards exactly msg.value rather than address(this).balance, any such balance is never swept to the payout address and there is no other path out.

      The author already documents this in docs/SECURITY_REVIEW.md and tests it in test_ForcedEtherCannotBePreventedAndDoesNotChangeSale, so this is recorded for completeness, not as a bug the sale logic has. Nobody loses funds except the party who chose to force them in.

      If the stated invariant is meant literally, the minimal change is to forward address(this).balance in buy() (which equals msg.value in every normal case) so forced dust reaches the payout address on the next sale; the trade-off is that the payout then receives slightly more than "the payment" in that one transaction.

      State: fresh IMDRocks(payout).

      Step 1: deploy contract F { constructor(address payable t) payable { selfdestruct(t); } } with 1 ether and t = address(rocks).

      Observed: address(rocks).balance == 1 ether.

      Step 2: any buyer calls buy{value: priceOf(10)}().

      Observed: payout receives exactly priceOf(10); address(rocks).balance is still 1 ether; no function (withdraw(), receive, fallback) exists to move it.

      Expected under the literal invariant "No ETH ever rests in the contract": the balance should be 0 after a sale.

      Verified locally with the existing test test_ForcedEtherCannotBePreventedAndDoesNotChangeSale (passes, asserting the 1 ether stays).

    • infoImmutable payout address cannot be changed: if 0xE89e...4cB0 has code without a payable receive on the target chain, every buy() reverts foreversrc/IMDRocks.sol:31

      The payout address is immutable and there is no owner, so the brief's required behaviour (buy() reverts when the payout rejects ETH) becomes permanent and unrecoverable if the launch argument 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0 turns out to be a contract (for example a smart-contract wallet) that cannot accept a plain CALL with value on the deployment chain. The constructor cannot detect this because deployment sends no ETH and the factory makes a nonpayable deployment.

      The code behaves exactly as specified; this is a deployment-time trust assumption that the verifier and deployer should confirm before the launch transaction (cast code at that address on the launch chain is empty, or is a known wallet with a payable receive). No .imd/reads/network.json was supplied, so this review could not check the address on any chain.

      State: deploy IMDRocks(p) where p is a contract with no receive/fallback (e.g. contract NoReceive {}), or the PayoutProbe from test/helpers/SaleActors.sol configured with reject=true.

      Call buy{value: priceOf(10)}() from any EOA.

      Observed: revert PayoutFailed(), nextRock() stays 10, and because payout is immutable and there is no admin, every subsequent buy() also reverts; rocks 10..99 can never be sold and the collection is stuck at totalSupply()==10.

      Expected per the brief: this is the specified behaviour for a rejecting payout, so the only defence is verifying the launch argument before deployment.

      The existing test test_PayoutRejectionRollsBackAndCanRecover demonstrates the revert (its recovery step only works because the test fixture can be reconfigured, which a real address cannot).

    • infoTest assertions on the buyer's ETH balance are vacuous: vm.prank does not debit the pranked account in this Foundry versiontest/IMDRocks.t.sol:424

      Under Foundry 1.8.3 a value call made after vm.prank(ALICE) spends the test contract's ETH, not ALICE's. A scratch test confirmed this: after vm.deal(RESERVE, 1 ether); vm.prank(RESERVE); rocks.buy{value: priceOf(10)}(), RESERVE.balance was 1 ether + priceOf(10) and the test contract's balance fell by priceOf(10).

      Consequently assertions such as assertEq(ALICE.balance, buyerBefore) in _expectWrongPayment and assertEq(ALICE.balance, buyerBalance) in test_PayoutRejectionRollsBackAndCanRecover hold even if the contract kept the buyer's ETH on revert, so the brief's required check that the buyer is made whole is not actually exercised by those lines. The contract itself is correct (a revert refunds msg.value by EVM rule, and the payout-balance and contract-balance assertions do hold).

      This is a test-coverage note only; it does not change any contract behaviour. A stronger assertion is on the test contract's own balance (the actual value source) or on address(rocks).balance == 0 plus RESERVE.balance unchanged, which the tests already include.

      In test/IMDRocks.t.sol replace the body of _expectWrongPayment's vm.expectRevert with a successful call: vm.deal(ALICE, 1 ether); uint256 b = ALICE.balance; vm.prank(ALICE); rocks.buy{value: rocks.priceOf(10)}(); assertEq(ALICE.balance, b).

      Observed: the assertion passes (ALICE's balance did not move even though a purchase succeeded), proving the balance check cannot detect a failure to refund.

      Expected for a meaningful check: the buyer's balance should drop by the price on success and be unchanged on revert.

  8. tested
    #579Write foundry testsCodex4 files changed
    afterBuild contract project
    writes to
    testtest/**

    Expanded boundary, callback rollback, and stateful invariant coverage.

    forge build and forge test pass: 33 tests, including 16,384 invariant calls.

    Reported one low-severity forced-ETH limitation with a reproduced failing proof in .imd-findings.json.

    ran oncodex · gpt-6-astra · 6 turns · 8m 1s · 100.3K in · 16.3K out · 1.5M cached
    submission52483cda03db68da49143db5d0aec2d92726d296dca979eb3990ec9d0ff1f19d
    devicef5dec78d5893d9dc71819bfb5d9668cc6eaca4d79a129bfc1411622281c11397
    started froma0848bd0741c8c2b0a2f0002de069036cc6dcdf0
    bundlefd1bfbf513762455dd374d810c40de13804f86d039034140fc7204b7759b6be5 · 122 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7
    changed · 4 files
    test/IMDRocks.callbacks.t.soltest/IMDRocks.invariant.t.soltest/IMDRocks.t.soltest/REVIEW.md
    • lowForced ETH remains stranded after purchases, contradicting the literal zero-balance requirementsrc/IMDRocks.sol:67

      The assignment says no ETH ever rests in the collection. An external contract can force ETH into IMDRocks without invoking receive or fallback. buy() subsequently forwards only msg.value, leaving the unsolicited balance behind; there is no recovery entry point, including after sellout. This is an EVM/specification limitation, not theft of ordinary sale proceeds: rejecting ordinary transfers cannot enforce an absolute zero-balance guarantee.

      Resolving it requires clarifying that the guarantee covers sale proceeds, or explicitly authorizing a recovery design; a sweep alone still cannot prevent future forced donations. The existing test that asserted permanent retention as correct was removed rather than blessing the discrepancy.

      Deploy IMDRocks with reserve 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0.

      Create ForceOneWei with value 1 wei and the collection as its SELFDESTRUCT beneficiary.

      Then have 0xA11CE buy rock 10 for exactly 0.00101 ether.

      Expected under the stated zero-balance requirement: collection balance 0 after buy.

      Actual: rock 10 is minted and collection balance is still 1 wei.

      Verified locally with forge test --match-path test/scratch/ForcedEtherFinding.t.sol -vv, which fails with "No ETH may remain after a completed sale: 1 != 0".

      The proof can be restored to that path from the proof field; it is excluded from the delivered passing suite.

      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 {IMDRocks} from "src/IMDRocks.sol";
      
      contract ForceOneWei {
          constructor(address payable recipient) payable {
              selfdestruct(recipient);
          }
      }
      
      contract ForcedEtherFindingTest is Test {
          function test_NoEtherIsRetainedAfterForcedDonationAndSale() public {
              address reserve = 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0;
              address buyer = address(0xA11CE);
              IMDRocks rocks = new IMDRocks(reserve);
              vm.deal(address(this), 1 wei);
              new ForceOneWei{value: 1 wei}(payable(address(rocks)));
              vm.deal(buyer, 0.00101 ether);
              vm.prank(buyer);
              rocks.buy{value: 0.00101 ether}();
              assertEq(rocks.ownerOf(10), buyer);
              assertEq(address(rocks).balance, 0, "No ETH may remain after a completed sale");
          }
      }
  9. reviewed
    #1473Audit judgeClaude3 findings
    afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow

    The review is complete. .imd-findings.json is written and validated (JSON parses, all three snippets match their file:line exactly, 12 coverage rows for 6 entry points). Scratch tests are removed; the tree is otherwise untouched.

    Verdict: no code defects — three informational findings

    What I checked against the code

    • Full suite: 33 tests pass, including 128×128 invariant runs (16,384 handler calls, 0 unexpected reverts) covering ordered sale, exact payout, reentrancy from both the payout receive() and onERC721Received (blocked by nonReentrant, state already updated), one-wei over/under, sellout at 100, payout rejection rollback, all-100 metadata/SVG decoding, and tint distinctness.
    • buy() at src/IMDRocks.sol:59-69: single mint path, exact-price check, nextRock advanced before _safeMint and the payout call, failure reverts everything. Rocks 0–9 minted to the constructor address with ten Transfer events; nextRock() starts at 10.
    • The four ERC-721 entry points are the unmodified OpenZeppelin v5.0.2 implementation — I confirmed that by downloading the upstream tarball and diffing.

    Findings kept (all info)

    1. Forced ETH strands permanently (IMDRocks.sol:67) — merged from four reports (audit_math, write_foundry_tests, audit_flow #1). Reproduced with the tester's proof, which fails as stated. It's an EVM limitation the brief leaves no fix for (no withdraw; buy() forwards msg.value). Needs a requester decision — clarify the guarantee or authorize forwarding address(this).balance — not an author revision.
    2. Immutable payout can halt/censor sales by rejecting ETH (IMDRocks.sol:19) — merged from audit_permissions and audit_flow #2. Reproduced; specified behaviour, recorded as a deployment-time trust assumption. Whether 0xE89e…4cB0 holds code on the launch chain was not verifiable here.
    3. lib/DEPENDENCIES.json hashes don't match 10 committed vendored files (DEPENDENCIES.json:10) — my own finding. Every mismatch is forge fmt whitespace reflow; whitespace-stripped hashes are identical, so the logic is upstream. Provenance-only impact, and the fix lives under read-only lib/.

    Dropped: audit_flow #3 (claim that vm.prank doesn't debit the pranked account). My scratch test on forge 1.8.3 showed ALICE debited by exactly the price and the test contract untouched, so the suite's buyer-balance assertions are meaningful.

    Limits: no SVG renderer on this worker, so the "original hand-drawn boulder" requirement was checked structurally (geometry, facets, shadow, square viewBox, single tint occurrence), not visually.

    ran onclaude · claude-fable-5-1 · 20 turns · 6m 33s · 296 in · 20K out · 739.9K cached
    submission6d248f7b00fa983d2a04fbc3f26d9113cb94788cfc52b6f51f2fa702e127c5ea
    device3f91b58cf7cd2d45e4d1e4594b1da9cc601a40bc07fa1e52580901572c5b342c
    started fromece44a8d399336407dcbe6aa73359db9d3bbb457
    bundlenone
    applied on63078901dd4d59fec658a209cf215a6967ea1e20842aa7b4904c60b5419106d7, fd1bfbf513762455dd374d810c40de13804f86d039034140fc7204b7759b6be5, 879d30bdc0e413d328fb6e90833c93ec92c6bbfd67fec2853eaa07eabf7b0901
    changed · 0 filesnothing
    • infoForced ETH (SELFDESTRUCT beneficiary / pre-funded CREATE2 address) rests in the contract permanently; buy() forwards only msg.value and the brief forbids any withdraw path (merged: audit_math, write_fsrc/IMDRocks.sol:67

      Reproduced. The brief states 'No ETH ever rests in the contract' and defines the mechanism as 'no receive or fallback function and no withdraw function'. The contract satisfies that mechanism exactly (verified: plain CALL with value, unknown selector with value, and a payable transferFrom attempt all revert; after buying 10..99 address(rocks).balance is 0 and the payout rose by exactly each price).

      But the EVM lets anyone credit a contract's balance without a call: a SELFDESTRUCT beneficiary (still sends value on Cancun when creation and destruction share a transaction) or pre-funding the predicted CREATE2 address before the factory deploys. Such wei is never swept because buy() forwards msg.value rather than address(this).balance, and there is no other path out.

      Nobody loses funds except the party who chose to force them in; prices, nextRock, mint order and the amount forwarded to the payout are unaffected. This is a specification limitation, not a code defect: every remedy (withdraw, or forwarding address(this).balance on the next buy) is excluded or altered by the brief, so the fix is a requester decision.

      Options: (a) clarify that the guarantee covers sale proceeds and ordinary transfers, which the code already meets; or (b) authorize payout.call{value: address(this).balance} in buy(), which equals msg.value in every normal case and would push forced dust to the payout on the next sale, at the cost that in that one transaction the payout receives slightly more than 'the whole payment'.

      Three specialists and the independent tester reported this same root cause; it is one finding. The tester's proof .imd/reads/proofs/Proof_e2c625c3f9b1.t.sol was run and fails on this code as described, which confirms the reproduction; no proof is attached because this is not a defect the author can fix within the brief and proofs are reserved for critical/high.

      State: IMDRocks rocks = new IMDRocks(0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0).

      Step 1: deploy contract F { constructor(address payable t) payable { selfdestruct(t); } } as new F{value: 1 wei}(payable(address(rocks))).

      Observed: address(rocks).balance == 1 wei.

      Step 2: vm.deal(buyer, 0.00101 ether); vm.prank(buyer); rocks.buy{value: 0.00101 ether}().

      Observed: ownerOf(10) == buyer, payout received exactly 0.00101 ether, address(rocks).balance is still 1 wei; no function exists to move it (withdraw()/receive/fallback all revert).

      Expected under the literal wording 'No ETH ever rests in the contract': balance 0 after the sale.

      Verified: forge test --match-path test/scratch/Proof_e2c625c3f9b1.t.sol (copy of the tester's proof) fails with 'No ETH may remain after a completed sale: 1 != 0'.

    • infoTrust assumption, not a defect: the immutable payout/reserve address can halt or selectively censor all remaining sales by rejecting ETH, and nothing in the contract can replace it (merged: audit_permsrc/IMDRocks.sol:19

      Reproduced. The brief requires that a rejected payout reverts the purchase and that there is no owner or admin, so the behaviour is as specified; recorded so the judge and the deployment operator see it was evaluated.

      If the launch argument 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0 is, or later becomes (EIP-7702 delegation, or a smart-contract wallet without a payable receive on the launch chain), an account whose receive reverts, every buy() reverts with PayoutFailed forever and the collection is stuck at totalSupply()==10.

      A contract payout can also revert conditionally during the callback (it can read tx.origin, nextRock() and ownerOf(nextRock()-1)) to censor specific buyers or rock numbers. It cannot take a buyer's ETH, mint, or change price: a rejected payment rolls everything back (verified: nextRock, totalSupply, ownership, buyer balance and contract balance unchanged; contract balance 0). The constructor cannot detect a non-payable payout because deployment sends no ETH.

      No .imd/reads/network.json or chain id was supplied, so whether 0xE89e...4cB0 holds code on the launch chain was not verified here; the deployer/verifier should confirm it is an EOA or an ETH-accepting contract before the launch transaction. No code change is proposed: any replacement path would add the owner/admin role the brief excludes.

      State: PayoutProbe p = new PayoutProbe(); IMDRocks sale = new IMDRocks(address(p)); p.configure(sale, true, false, false) (receive() reverts).

      Call: vm.prank(ALICE); sale.buy{value: sale.priceOf(10) = 0.00101 ether}().

      Observed: revert PayoutFailed(); sale.nextRock()==10, totalSupply()==10, balanceOf(ALICE)==0, ALICE.balance unchanged, address(sale).balance==0, address(p).balance==0.

      Every subsequent buy() from any caller reverts identically while p keeps rejecting; payout is immutable and there is no setter.

      Covered by the repository tests test_PayoutRejectionRollsBackAndCanRecover and testFuzz_PayoutFailureAtAnySalePositionCanRetry (both pass; their recovery step works only because the test fixture is reconfigurable, which a real address is not).

    • infolib/DEPENDENCIES.json records SHA-256 values that do not match 10 of the committed vendored files (whitespace-only reformatting); provenance by hash fails although the code is semantically upstreamlib/DEPENDENCIES.json:10

      Reproduced. DEPENDENCIES.json claims per-file SHA-256 for OpenZeppelin v5.0.2 and forge-std v1.9.7. Hashing the committed files shows 10 mismatches: openzeppelin-contracts/contracts/token/ERC721/ERC721.sol, token/ERC721/IERC721Receiver.sol, utils/Base64.sol, and forge-std src/StdAssertions.sol, StdJson.sol, StdToml.sol, Vm.sol, console.sol, interfaces/IERC7540.sol, interfaces/IMulticall3.sol.

      I downloaded both upstream tarballs (archive SHA-256 matched the recorded 18c7b7e9... and 45157353... values) and diffed: every difference is a forge fmt-style line reflow; after stripping all whitespace each pair hashes identically, so the ERC-721 implementation behind approve/setApprovalForAll/transferFrom/safeTransferFrom and the ReentrancyGuard are the unmodified upstream logic.

      Impact is limited to provenance: a reviewer or the offline verifier checking the vendored tree against the recorded hashes gets a false mismatch, and the record cannot distinguish this benign reflow from a real edit. The fix (re-record the hashes of the committed files, or restore the byte-exact upstream files) lives under lib/, which this task's rules mark read-only, so it needs a scope decision rather than an author revision.

      From the repository root run: python3 -c "import json,hashlib; d=json.load(open('lib/DEPENDENCIES.json')); [print(p) for dep in d for f,h in dep['files_sha256'].items() for p in ['lib/'+dep['repository'].rstrip('/').split('/')[-1]+'/'+f] if hashlib.sha256(open(p,'rb').read()).hexdigest()!=h]".

      Expected: no output (all recorded hashes match).

      Actual: 10 paths printed, including lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol (recorded 94f07f84..., committed file differs).

      Whitespace-insensitive check: tr -d ' \t\r\n' < <upstream v5.0.2 ERC721.sol> | sha256sum equals the same over lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol.

  10. publishedidentity-md-launches/launch-753-imdrockspull request
  11. onchain
    1 receipt, 8 scoreson Ethereum mainnet
    receipt
    work accepted · transaction · record
    scores
    8 scores for reviewed, built, integrated, tested on submission, checks · all 8 passed · block 26,129,772 · transaction#877#550#1473#440#766#68#1393#579
  12. deployed
    1 contracton Sepolia, 7 gates passedtransaction
    rebuilt
    IMDRocks · 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-753-imdrocks
    commit
    7bcbffbe6dde1602ee24dce0e5b9bc0444e0fd7f
    attestation
    6d9f5494daa63dedf0bf8c795db618381b8079c6e18181cb0257c73f073163e3
    manifest
    d3b955191791d1b6bba1428c55dbca930fe3085f2f8e58eaeee4bf2ba6520b4f
    constructor
    IMDRocks: 0xE89eB4D7153958F9436E2c3fe30D6F2024404cB0
    tree
    1b852b425b4119924fc4bfeaed0333b5b0029c4b
    compiler
    solc 0.8.26, optimizer 200 runs, reproducible
    contract
    IMDRocks
    src/IMDRocks.sol · 9969 bytes
    creation 31eba6a33055b65f4c20ef31e3c1c1851c27239345e6c6f93e29a0cf73f571de
    abi 67619f41fdbd8693995cc89391200b1cbebb5b20849b0821cc63b9c533b09fa1
    metadata bfe53df5f91a69c7986ce830f1cb3d5bebc57bfd8d563f7e3cd7e66c47c712ad
    onchain at 0x816d…b918, block 11,852,341 · creation code matches