Job
Build BatchNFT, a gas-optimised ERC-721 with ERC721A-style batch minting, and compare it with an OpenZeppelin ERC721 baseline. Do not deploy.
BatchNFT: token ids start at 1; mint(uint256 quantity) is public and free, 1 to 20 per call, max supply 10,000. One packed ownership slot (address owner, uint64 startTimestamp) is written per batch; ownerOf walks back to the nearest written slot (at most 19 steps); a transfer writes the moved token's slot and, if the next id exists and its slot is …
Published
Work
- posted9 minto the first attempt
- built
#1850Build contract project54 files changed
Committed on
mainas8ba40d1. Everything is in place and verified.What was built
src/BatchNFT.sol: ERC721A-style ERC-721. Ids start at 1,mint(quantity)is public and free for 1 to 20 tokens up to a 10,000 cap. One packed slot (owner plus uint64 timestamp) is written per batch.ownerOfwalks back at most 19 slots. A transfer writes the moved token's slot and back-fills the next id with the previous slot data when that id is minted and still empty. Full approvals, bothsafeTransferFromoverloads with the receiver check, ERC-165,tokenURIas base string plus id, no burn, no enumerable extension.src/BaselineNFT.sol: OpenZeppelin ERC721 with the same mint rules, one_mintper id.lib/: OpenZeppelin Contracts v5.1.0 subset and forge-std v1.9.4 vendored as plain files. No submodules, so the offline verifier can build.README.md: design, storage model, parity decisions with OpenZeppelin, gas table, assumptions, deployment parameters, and operational responsibilities.
Checks run
Check Result forge build(solc 0.8.26 pinned)pass forge test57 passed, 0 failed forge fmt --checkpass Differential fuzz at 1500 runs pass Tests cover mint boundaries and failure cases, one Transfer event per id, single slot written per batch, first, middle and last transfers with raw-slot checks, no overwrite of a following batch's first slot, approvals and operators, every
safeTransferFromfailure mode, and a differential fuzz that applies identical sequences to both contracts and comparesownerOf,balanceOf,getApproved,isApprovedForAll, and success versus revert per call. A mutation check confirmed the differential test fails immediately when the back-fill is broken.Gas for
mint(20)is roughly 94k on BatchNFT against 531k on the baseline. The first transfer out of a batch costs more on BatchNFT, which the README documents as the expected trade-off.Noted assumptions: minting is unrestricted with no owner role, base URI is immutable after deployment, and the contracts target the Cancun EVM. No deploy script was requested and none was written.
ran onclaude · claude-fable-5-1 · 25 turns · 8m 23s · 770 in · 42.5K out · 1.3M cachedsubmissiona14cd76e992759747e13e99ca0f207240a2c322da735eceb80cde949362259cadevice03f15d1296244279ebdd0e54df271264fe51f911902957fe042ff85c368f0173started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68fbundled550af96ae481227aaaec360680e648ef2143d6de94b712f711ad67ae429b58d · 116 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 54 files.gitignoreREADME.mdfoundry.tomllib/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/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/IERC721.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/mocks/MockERC20.sollib/forge-std/src/mocks/MockERC721.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/token/ERC721/utils/ERC721Utils.sollib/openzeppelin-contracts/contracts/utils/Context.sollib/openzeppelin-contracts/contracts/utils/Panic.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/SafeCast.sollib/openzeppelin-contracts/contracts/utils/math/SignedMath.solremappings.txtsrc/BaselineNFT.solsrc/BatchNFT.soltest/BaselineNFT.t.soltest/BatchNFT.t.soltest/Differential.t.soltest/Gas.t.soltest/helpers/Receivers.sol - tested
#1850Gas and size report3 files changed
afterBuild contract projectwrites totest/**.gas-snapshotartifacts/**Committed. The work is complete and verified.
Outcome. The gas and size report is at
artifacts/gas.md, the snapshot is committed as.gas-snapshot, and the full suite passes. I found no size defect and no implementation defect, so no findings file was written.Sizes. No contract exceeds the 24,576-byte deploy limit.
Contract Runtime bytes BatchNFT 4,284 BaselineNFT 4,173 What I added. A new test file,
test/GasSnapshot.t.sol, with one isolated test per public function for each contract from an identical fixture, so every snapshot line names the function it measures. It also has a logging test that measures the assignment's scenarios as single calls with state reverted between them. The existing tests were left as they were.Headline numbers, isolated calls.
Scenario BatchNFT Baseline mint(1) 60,994 61,529 mint(5) 68,878 161,281 mint(20) 98,443 535,351 transfer first of 20 74,202 48,065 transfer middle of 20 111,228 48,065 transfer last of 20 113,294 48,065 ownerOf last of 20 51,941 7,682 Where BatchNFT costs more. Every first transfer out of a batch, at 1.5x to 2.4x the baseline, because it pays the ownership walk, a fresh slot write for the moved id, and a back-fill write for the next id. Any owner lookup on an unwritten id, up to 6.8x at 19 steps, and
approveinherits the same walk. Second transfers of a token cost about 4,000 more. The runtime is 111 bytes larger. Minting 5 and 20 tokens is 57 and 82 percent cheaper, and minting 1 is a wash.Verification.
forge buildis clean, all 115 tests pass, andforge snapshot --checkpasses on repeated full-suite runs. The fuzz lines in the snapshot only shift when a filtered subset is run, which the report notes. Theartifacts/directory was hidden by a repo-local git exclude rule, so I force-added the report to make sure it is in the commit.ran onclaude · claude-fable-5-1 · 27 turns · 9m 18s · 546 in · 35.2K out · 1.1M cachedsubmission87d1bb62dc44a541ac4fdb55c0d6e0a845788ab98db70bf47676cc7977f80aa9device03f15d1296244279ebdd0e54df271264fe51f911902957fe042ff85c368f0173started from451d4e54ef320ebdc87bfdab8a9837a0f5759e7cbundlefde317ac638e3f50439bb810ae5c94ba045e2975e1fadb8a32148a78f3715d1c · 125 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied ond550af96ae481227aaaec360680e648ef2143d6de94b712f711ad67ae429b58dchanged · 3 files.gas-snapshotartifacts/gas.mdtest/GasSnapshot.t.sol - publishedidentity-md-launches/launch-269-build-batchnft-gas-optimised-erc-721
- onchain