{"id":47929400,"url":"https://github.com/0xferit/slot-recycling-lib","last_synced_at":"2026-04-04T07:12:54.717Z","repository":{"id":344840114,"uuid":"1183363150","full_name":"0xferit/slot-recycling-lib","owner":"0xferit","description":"Solidity library that recycles freed mapping slots via tombstoning, turning 20,000-gas zero-to-nonzero SSTOREs into 2,900-gas nonzero-to-nonzero writes.","archived":false,"fork":false,"pushed_at":"2026-03-16T17:38:06.000Z","size":46,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-17T02:25:34.314Z","etag":null,"topics":["compression","ethereum","evm","gas-optimization","library","optimization","quantization","solidity","uint"],"latest_commit_sha":null,"homepage":null,"language":"Solidity","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0xferit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-16T14:32:22.000Z","updated_at":"2026-03-16T17:37:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/0xferit/slot-recycling-lib","commit_stats":null,"previous_names":["0xferit/slot-recycling-lib"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/0xferit/slot-recycling-lib","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xferit%2Fslot-recycling-lib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xferit%2Fslot-recycling-lib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xferit%2Fslot-recycling-lib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xferit%2Fslot-recycling-lib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0xferit","download_url":"https://codeload.github.com/0xferit/slot-recycling-lib/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xferit%2Fslot-recycling-lib/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31390991,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T04:26:24.776Z","status":"ssl_error","status_checked_at":"2026-04-04T04:23:34.147Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compression","ethereum","evm","gas-optimization","library","optimization","quantization","solidity","uint"],"created_at":"2026-04-04T07:12:54.073Z","updated_at":"2026-04-04T07:12:54.704Z","avatar_url":"https://github.com/0xferit.png","language":"Solidity","funding_links":[],"categories":[],"sub_categories":[],"readme":"# slot-recycling-lib\n\n[![lifecycle (50% reuse): gas saved](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/0xferit/slot-recycling-lib/gh-badges/.badges/recycling-savings.json)](test/showcase/ShowcaseGas.t.sol)\n[![per recycled write: gas saved](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/0xferit/slot-recycling-lib/gh-badges/.badges/bestcase-savings.json)](test/showcase/ShowcaseGas.t.sol)\n\nEVM charges ~20,000 gas for a zero-to-nonzero SSTORE but only ~2,900 gas (warm) for nonzero-to-nonzero. In mapping-backed collections with churn, this library recycles freed slots by leaving a non-zero \"tombstone\" on deletion instead of fully zeroing. The next allocation overwrites the tombstoned slot at the cheaper rate.\n\n## How it works\n\nWhen a Solidity contract uses `delete` on a mapping entry, the slot is set to zero. The next time that slot is written, the EVM treats it as a zero-to-nonzero transition and charges the full 20,000 gas SSTORE cost.\n\nThis library avoids that by never fully zeroing a slot. On deletion, it clears only the bits you specify (via a \"clear mask\") and leaves the rest as a non-zero tombstone. The slot stays dirty, so the next write is a cheap nonzero-to-nonzero transition (~2,900 gas warm).\n\nTo tell vacant slots apart from occupied ones, the library uses a **vacancy flag**: a configurable bit range within the 256-bit word. When those bits are zero, the slot is vacant and available for reuse. Any packed struct that has a field guaranteed to be non-zero when occupied (e.g., an amount, a timestamp, an address) can serve as the vacancy flag.\n\nThe lifecycle:\n\n1. **Allocate**: scan from a hint index, find the first slot where the vacancy flag bits are zero, write the new packed value.\n2. **Free**: clear the vacancy flag bits (and optionally others) via a bitmask, leaving a non-zero tombstone.\n3. **Re-allocate**: the freed slot is found by the next scan and overwritten at the cheap SSTORE rate.\n\n## Gas economics\n\nPost-London (EIP-2929 + EIP-3529):\n\n| SSTORE transition | Warm | Cold |\n|---|---|---|\n| zero to nonzero | 20,000 | 22,100 |\n| nonzero to nonzero | 2,900 | 5,000 |\n\n### Benchmarks\n\nAll benchmarks come from `ShowcaseGas.t.sol` and run within a single transaction (warm storage).\n\n**Lifecycle: 20 creates, 10 deletes (50% reuse rate)**\n\nSimulates a content board with churn. 10 articles are created, 5 deleted, 5 created (reusing freed\nslots), 5 more deleted, 5 more created (reusing again). Of the 20 total creates, 10 are fresh writes\nand 10 land on recycled slots.\n\n| | Total gas | Savings |\n|---|---|---|\n| Raw (standard delete) | 510,710 | |\n| Recycled (tombstone) | 324,863 | **36.4%** |\n\n**Per-write: create-after-delete (best case, zero scan)**\n\nIsolates the single-write savings. Create one article, delete it, create another. The recycled path\nfinds the freed slot immediately with no scan.\n\n| | Gas | Savings |\n|---|---|---|\n| Raw (full-zero delete) | 23,613 | |\n| Recycled (tombstone) | 2,800 | **88.1%** |\n\nThe per-write savings are up to 88%, but lifetime savings depend on your reuse rate: how often a\ncreate lands on a recycled slot vs. a fresh one. The per-write benchmark assumes zero scan iterations;\nthis is realistic in practice because `findVacant` (a view function) can locate the next vacancy\noff-chain, and the on-chain `allocate` call starts at that exact index.\n\nRun the benchmark:\n\n```bash\nforge test --match-path test/showcase/ShowcaseGas.t.sol -vv\n```\n\n## Quick start\n\n```solidity\nimport {RecycleConfig, SlotRecyclingLib} from \"slot-recycling-lib/src/SlotRecyclingLib.sol\";\n\n// Vacancy field spans bits 192-247 (56 bits) of the packed word.\nRecycleConfig private immutable CFG = SlotRecyclingLib.create(192, 56);\nSlotRecyclingLib.Pool private _pool;\n\n// Allocate: scans from hint, writes to first vacant slot.\nuint256 idx = SlotRecyclingLib.allocate(_pool, CFG, 0, packedValue);\n\n// Free: clears vacancy flag bits, leaves tombstone (slot stays non-zero).\nSlotRecyclingLib.free(_pool, CFG, idx, CLEAR_MASK);\n\n// Next allocate reuses the freed slot at ~2,900 gas instead of ~20,000.\n```\n\n## Before you integrate\n\n\u003e **This library changes the observable semantics of a mapping-backed collection.**\n\u003e Read this section before adopting it; the storage optimization is not free of tradeoffs.\n\n### Semantic differences from a normal mapping\n\n| Normal mapping | SlotRecyclingLib pool |\n|---|---|\n| Each new entry gets a fresh, never-before-used key | Slot indices are **reused**. A new allocation may return an index that previously belonged to a different logical item. |\n| `delete` zeroes the slot; reading it returns `0` / default | `free` / `freeWithSentinel` leave a **non-zero tombstone**. Reading a freed slot returns stale data, not zero. |\n| A zero read reliably means \"does not exist\" | A zero read only means the slot was **never written**. Freed slots read as the tombstone, not zero. |\n| IDs are inherently monotonic (e.g., `nextId++`) | Recycled indices are **not monotonic**. Do not use slot indices as externally visible unique IDs without an indirection layer. |\n\n**If your contract or off-chain indexer relies on any of the left-column behaviors, you must add your own bookkeeping.** Common mitigations:\n- Maintain a separate monotonic counter and map external IDs → slot indices.\n- Track existence with a `mapping(uint256 =\u003e bool)` or a bitmap alongside the pool.\n- Treat any read where `isVacant(pool, cfg, index)` returns `true` as \"does not exist.\"\n\n### Operational footguns\n\n1. **Double-free is silently permitted.** `free` and `freeWithSentinel` do not check whether the slot is already vacant. Calling free twice on the same index succeeds as long as the resulting tombstone is non-zero. Guard against this in your own code if double-free would break your invariants.\n\n2. **`store` bypasses all invariants.** It performs a raw `SSTORE` with no vacancy check and no vacancy-flag validation. Writing zero or a value with vacant vacancy bits corrupts the pool — the slot will appear vacant while holding data, or vice versa. Use `store` only for migrations or administrative overrides, never in normal allocation paths.\n\n3. **Vacancy bits must be non-zero for every occupied value.** `allocate` enforces this, but if you construct packed values incorrectly the check will revert your transaction. Pick a field that is *guaranteed* non-zero whenever the slot is logically occupied (e.g., a non-zero amount, a non-zero timestamp, a non-zero address).\n\n4. **`delete` or any full-zero write defeats the optimization.** If any code path writes zero to a pool slot (Solidity `delete`, inline assembly `sstore(slot, 0)`), the next write to that slot will pay the full 20,000 gas zero-to-nonzero cost. Always use `free` or `freeWithSentinel` to clear slots.\n\n5. **Gas savings depend on reuse rate and hint quality.** If your workload rarely deletes, or the `searchPointer` hint is far from the next vacancy, the scan overhead can offset or exceed the savings. Benchmark with your actual access pattern (see [ShowcaseGas.t.sol](test/showcase/ShowcaseGas.t.sol)).\n\n### Choosing between `free` and `freeWithSentinel`\n\n| Use `free` when | Use `freeWithSentinel` when |\n|---|---|\n| At least one field naturally stays non-zero after clearing the vacancy field and other mutable data (e.g., an `address owner` field). | No remaining field is guaranteed non-zero, or you want a deterministic tombstone value across all slots. |\n| You want to preserve some original data as part of the tombstone (e.g., keep the owner address for historical queries). | You want a fixed, recognizable sentinel (e.g., `0x01`) that is trivial to filter out in off-chain indexing. |\n\n### Constructing a safe `clearMask`\n\nThe `clearMask` passed to `free` tells the library which bits to zero. The bits that remain form the tombstone.\n\n1. **Always include the vacancy flag bits.** If the clear mask does not cover every vacancy-flag bit, `free` reverts with `ClearMaskIncomplete`.\n2. **Include all mutable data fields** you want to erase — but leave at least one non-zero field untouched so the tombstone is non-zero.\n3. **Build the mask with `SlotRecyclingLib.bitmask`** and compose ranges with bitwise OR:\n   ```solidity\n   // Clear bountyAmount (bits 192-247) and withdrawalPermittedAt (bits 160-191).\n   // Leaves owner (bits 0-159) and category (bits 248-255) as tombstone.\n   uint256 CLEAR_MASK = SlotRecyclingLib.bitmask(192, 56) | SlotRecyclingLib.bitmask(160, 32);\n   ```\n4. **Choose a vacancy field that your contract guarantees is non-zero when occupied.** Good candidates: a non-zero token amount, a timestamp field that your contract never leaves at zero for occupied entries, or a non-zero-address owner. Avoid boolean fields (only 1 bit wide and not byte-aligned) or fields that can legitimately be zero.\n\nSee [`RecycledArticleStore.sol`](src/showcase/RecycledArticleStore.sol) for a complete working example, and [`RawArticleStore.sol`](src/showcase/RawArticleStore.sol) for the standard-mapping baseline it replaces.\n\n### When NOT to use this library\n\nUse this checklist to decide whether slot recycling fits your use case:\n\n- [ ] **Your mapping has meaningful churn** (entries are created and deleted regularly). If entries are append-only, there are no slots to recycle.\n- [ ] **You can identify a non-zero vacancy field** in your packed struct. If every field can legitimately be zero when occupied, tombstoning does not work cleanly.\n- [ ] **Your contract does not depend on zero-on-missing semantics.** If you rely on reading a deleted key as zero (e.g., for access-control checks like `require(balances[id] == 0)`), tombstone data will break that assumption.\n- [ ] **Slot indices are not used as external unique IDs** — or you have an indirection layer that maps stable external IDs to recycled internal indices.\n- [ ] **Off-chain indexers can handle non-zero reads on freed slots** or you have existence tracking that indexers can query.\n\nIf any box stays unchecked, consider whether the integration cost outweighs the gas savings.\n\n## Installation\n\n```bash\nforge soldeer install slot-recycling-lib\n```\n\n## Solidity API\n\nLibrary: `SlotRecyclingLib` (`src/SlotRecyclingLib.sol`). Import both the `RecycleConfig` type and the library.\n\nBecause the source file declares `using SlotRecyclingLib for RecycleConfig global`, importers get method-call syntax on config values automatically.\n\n### Type layout\n\nThe `RecycleConfig` value type wraps a precomputed `uint256` vacancy mask. The mask is computed by\n`create(offset, width)` and has `width` consecutive bits set starting at bit `offset`.\nA slot is vacant when `slotData \u0026 vacancyMask == 0`.\n\n**Byte-alignment:** both offset and width must be multiples of 8. This is a deliberate design choice\nto align with Solidity's native packed types (uint8 through uint248), where field boundaries always\nfall on byte boundaries. Sub-byte vacancy flags (e.g., a single bool bit) are not supported.\n\n### API\n\n| Function | Description |\n|---|---|\n| `SlotRecyclingLib.create(offset, width)` | Creates a `RecycleConfig`. Reverts with `BadRecycleConfig` on invalid parameters. |\n| `cfg.vacancyMask()` | Returns the precomputed vacancy mask. |\n| `SlotRecyclingLib.bitmask(offset, width)` | Returns a mask with `width` bits set at `offset`. Compose with OR for clearMask arguments. |\n| `allocate(pool, cfg, searchPointer, packedValue)` | Scan from hint, write to first vacant slot. Reverts if vacancy bits in value are zero. |\n| `free(pool, cfg, index, clearMask)` | Clear bits via mask, leave tombstone. Reverts if tombstone would be zero. |\n| `freeWithSentinel(pool, cfg, index, sentinel)` | Write fixed sentinel as tombstone. For cases where no field naturally stays non-zero. |\n| `load(pool, index)` | Raw read of packed value. |\n| `store(pool, index, packedValue)` | Raw write (no vacancy scan). |\n| `isVacant(pool, cfg, index)` | True if vacancy flag bits are all zero. |\n| `findVacant(pool, cfg, searchPointer)` | Scan for next vacant slot (view, for off-chain hints). |\n\n### Errors\n\n```solidity\nerror BadRecycleConfig(uint256 vacancyBitOffset, uint256 vacancyBitWidth);\nerror TombstoneIsZero();\nerror VacancyFlagNotSet(uint256 packedValue);\nerror ClearMaskIncomplete(uint256 clearMask);\nerror SentinelOccupied(uint256 sentinel);\n```\n\n## Showcase\n\nShowcase contracts under `src/showcase/` compare:\n\n- `RawArticleStore`: standard mapping with `delete` on removal.\n- `RecycledArticleStore`: same API using `SlotRecyclingLib` for tombstoned recycling.\n  Scans from 0 on every allocation to isolate the recycling benefit.\n- [`RecycledArticleStoreWithHint`](src/showcase/RecycledArticleStoreWithHint.sol): production-oriented\n  example with a `_nextHint` strategy that keeps scans tight. Shows the recommended pattern for\n  real integrations.\n\n### Hint strategy\n\n`RecycledArticleStoreWithHint` maintains a `_nextHint` state variable—the lowest index likely to be\nvacant:\n\n- **On allocate:** pass `_nextHint` as the search pointer; after allocation, set\n  `_nextHint = allocatedIndex + 1`.\n- **On free:** if the freed index is below `_nextHint`, move the hint down to the freed index.\n\nThis simple policy gives O(1) scan cost when slots are freed and re-allocated in FIFO order, and\ndegrades gracefully to a short linear scan when gaps are scattered. See the contract's NatSpec for\ntradeoff discussion. Run the benchmark:\n\n```bash\nforge test --match-path test/showcase/ShowcaseHintTest.t.sol -vv\n```\n\n## Stability \u0026 Semver\n\nThis library follows semver. Breaking changes to the documented public API\nrequire a **major** version bump. See [`STABILITY.md`](STABILITY.md) for the\nfull policy.\n\nA compile-time compatibility fixture (`test/compat/PublicApiCompat.t.sol`)\nexercises every supported import and call pattern. CI fails if the fixture\nstops compiling or its tests break.\n\n## Review status\n\nThis library is currently **unaudited**. The maintainer is open to an\nindependent security review, including a pro bono or discounted review if an\nauditor is interested in supporting public-good infrastructure, but no such\nreview has been commissioned or completed yet.\n\nUntil an external review artifact is published, treat this library as\nunaudited and review your own integration accordingly.\n\nReview materials:\n\n- [`SECURITY.md`](SECURITY.md)\n\n## License\n\nMIT (see SPDX headers in source files).\n\n## Author\n\n[0xferit](https://github.com/0xferit)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0xferit%2Fslot-recycling-lib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0xferit%2Fslot-recycling-lib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0xferit%2Fslot-recycling-lib/lists"}