Skip to content

Storage Layout

DISCLAIMER // NFA // DYOR

This analysis is based on observations of the contract behavior. We are not smart contract security experts. This document aims to explain what the contract appears to do based on the code. It should not be considered a comprehensive security audit or financial advice. Always verify critical information independently and consult with blockchain security professionals for important decisions.

⊙ generated by robots | curated by humans

METADATA
Contract Address 0x00000060...531524 (etherscan)
Network Ethereum Mainnet
Analysis Date 2026-08-08

Variables

The contract declares eight sequential storage slots. Solady's ERC721 and Ownable do not use sequential slots at all — both place their state at manually chosen high addresses to avoid collision with the inheriting contract, so they are listed separately below.

Sequential Slots (declared in TokenList)

SLOT VARIABLE NAME TYPE CURRENT VALUE PURPOSE
0 _extra mapping(uint256 => mapping(bytes32 => string)) 2 keys set (both on the Tacit listing) Open-ended per-listing metadata. The escape hatch that lets a new field cost a transaction rather than a redeployment
1 _extraKeys mapping(uint256 => bytes32[]) 2 entries on one listing Enumerable key set per listing, so a consumer can discover fields without knowing them in advance. Capped at 32 keys
2 _extraPos mapping(uint256 => mapping(bytes32 => uint256)) index+1 bookkeeping Position of each key in _extraKeys, stored as index+1 so 0 means absent
3 _tokens mapping(uint256 => Token) 17 populated entries The listing records themselves. Each Token occupies 8 consecutive derived slots
4 _ids uint256[] 0x11 (17) Array of every listed id. The length is the value stored in the slot itself
5 _position mapping(uint256 => uint256) index+1 per listed id Membership and position lookup. _position[id] == 0 is the canonical "not listed" test used by _mustExist
6 _boundLocalId mapping(address => uint256) 15 local bindings Maps a local token address to the listing id it resolves to. Normally identical to idOf(token), but an activated reservation keeps its hash-derived id, and this indirection preserves that
7 renderer + rendererLocked address (bytes 0–19) + bool (byte 20) 0x00000096...1eDcF4 (etherscan), false Two variables packed into one word. The renderer authoring every card, and the one-way flag that would freeze that choice

Manually-Placed Slots (inherited)

SLOT VARIABLE NAME TYPE CURRENT VALUE PURPOSE
0xffffffff...873927 _OWNER_SLOT address 0x006CD14F...8549F2 (etherscan) Solady Ownable owner. Computed as bytes32(~uint256(uint32(bytes4(keccak256("_OWNER_SLOT_NOT"))))), deliberately high to avoid collision
derived from 0x7d8825530a5a2e7a << 192 ERC-721 state balances, owners, approvals 17 tokens, balanceOf(registry) == 2 Solady ERC721 master slot seed. Ownership, balance and approval slots are keccak-derived from this seed rather than laid out sequentially

Token Struct Layout

Each listing occupies eight consecutive slots starting at keccak256(abi.encode(id, uint256(3))). Ten scalar fields are packed into a single word at offset +1, which is why a listing's flags, colour and rank can all be read in one SLOAD.

OFFSET FIELD(S) TYPE NOTES
+0 account bytes32 Token address right-aligned for EVM; a full 32-byte key for Solana mints and Bitcoin-rooted asset ids
+1 chainId, decimals, kind, standard, deployed, onchainSvg, synced, color, rank, frozen packed 176 bits used of 256. Byte offsets: 0–7 chainId, 8 decimals, 9 kind, 10 standard, 11 deployed, 12 onchainSvg, 13 synced, 14–16 color, 17–20 rank, 21 frozen
+2 name string Read from the token for local listings; curator-supplied for foreign ones
+3 symbol string Same provenance rule as name
+4 logo string The heaviest field. Bounded at 24,576 bytes; long values spill to keccak256(slot)
+5 url string Project link, bounded at 128 bytes
+6 audit string Optional audit link, bounded at 128 bytes. Empty on most listings
+7 description string Bounded at 256 bytes

Strings follow the standard Solidity short/long encoding: values of 31 bytes or fewer are stored inline with length * 2 in the low byte, and longer values store length * 2 + 1 in the slot with the data beginning at keccak256(slot).

The native ETH listing at id 0 demonstrates the whole layout in one read. Its +1 word is 0x0000000000000000000000000f4240627eea0000010100120000000000000001, which decodes to chainId = 1, decimals = 18, kind = EVM, standard = NATIVE, deployed = true, onchainSvg = false, synced = false, color = 0x627eea, rank = 1_000_000, frozen = false — matching summariesPaged exactly. Its +2 slot holds "Ether" inline, +3 holds "ETH", +4 holds 0x5ed (a 758-byte base64 logo stored out of line), +6 is zero because no audit link is set, and +7 holds a 69-byte description.

Id Space

Ids are not sequential. They are derived, and the two derivation rules are kept disjoint so that a consumer can tell them apart from the value alone.

ID CLASS DERIVATION RANGE COUNT
Local EVM listing uint256(uint160(token)) — literally the address < 2^160 16 (including native ETH at id 0)
Foreign / reservation keccak256(abi.encode(...)) \| (1 << 255) bit 255 set 1

A local EVM listing must fit in 160 bits; idOf(Kind, uint64, bytes32) reverts BadInput() if any of the upper 96 bits of an EVM account are set. Reservations use a separate tag, keccak256("zfi.token-list.reservation.v1"), mixed with the chain id and a curator-chosen key.


Diagrams

graph TB
    subgraph Extensions["Extension Fields - Slots 0-2"]
        S0["Slot 0: _extra<br/>id => key => string<br/>2 values set"]
        S1["Slot 1: _extraKeys<br/>id => bytes32[]<br/>max 32 keys"]
        S2["Slot 2: _extraPos<br/>id => key => index+1"]
    end

    subgraph Records["Listing Records - Slot 3"]
        S3["Slot 3: _tokens<br/>id => Token<br/>17 entries x 8 slots"]
        ST["keccak(id . 3) + 0..7<br/>account | packed | name<br/>symbol | logo | url<br/>audit | description"]
    end

    subgraph Index["Index & Membership - Slots 4-6"]
        S4["Slot 4: _ids<br/>uint256[] length = 17"]
        S5["Slot 5: _position<br/>id => index+1<br/>0 = not listed"]
        S6["Slot 6: _boundLocalId<br/>address => id<br/>15 bindings"]
    end

    subgraph Presentation["Presentation - Slot 7"]
        S7["Slot 7 packed:<br/>renderer 0x00000096...1eDcF4<br/>rendererLocked = false"]
    end

    subgraph Inherited["Inherited - Manual Slots"]
        SO["_OWNER_SLOT 0xffffffff...74873927<br/>0x006CD14F...8549F2"]
        SE["ERC-721 seed 0x7d8825530a5a2e7a &lt;&lt; 192<br/>owners | balances | approvals"]
    end

    S3 --> ST
    S4 --> S5
    S5 --> S6
    Extensions --> Records
    Records --> Index
    Index --> Presentation
    Presentation --> Inherited

    style S3 fill:#e1f0ff
    style ST fill:#e1f0ff
    style S7 fill:#fff4e1
    style SO fill:#ffe1e1

The grouping reflects how the contract actually uses these slots. Slots 0–2 exist only to make the extension mapping enumerable and removable in bounded time — delist walks every key, which is why setExtra caps the set at 32. Slots 4–6 are the membership and ordering layer: _position answers "does this exist", _ids supplies the iteration order that rankedIds sorts, and _boundLocalId handles the one case where an address does not resolve to its own id. Slot 7 is the only mutable pointer to code, and the only slot whose contents change what users see without changing what the registry stores.