abx.
Reference

Contracts

The reference implementation is small by design: a handful of interfaces, storage libraries, logic libraries, and extension mixins, assembled by inheritance into six concrete, deployable token contracts — three strict twin pairs, one per project shape — nothing else accomplishes the whole feature set.

A Series is many unique tokens (ERC-721). An Edition is many copies of a token (ERC-1155). Uniqueness maps to ERC-721, copies map to ERC-1155 — an orthogonal choice from the project shape (one artwork, many artworks, or generative/code) and from everything else a project picks.

Extensions are mixins, not a framework

Every opt-in piece of the event spine — Royalty, On-Chain Metadata, Max Invocations, and the rest — is one abstract contract in extensions/<name>/, and it is the whole extension: its private ID/VERSION constants, its _init<Name>(...) setup hook, its events (declared on a paired IAbx<Name> interface), and its own supportsInterface override. Nothing about an extension lives anywhere else, so adding one never touches a shared file. RoyaltyExtension is representative:

abstract contract RoyaltyExtension is AbxBeaconCore, ERC2981, Ownable, IAbxRoyalty {
    bytes32 private constant ID = 0x09e6...; // keccak256("abx.extension.royalty")
    uint16 private constant VERSION = 1;

    function _initRoyaltyExtension(address receiver, uint16 basisPoints) internal {
        _setExtensionVersion(ID, VERSION); // announce via the beacon
        _setRoyalty(receiver, basisPoints);
    }

    function supportsInterface(bytes4 interfaceId)
        public view virtual override(AbxBeaconCore, ERC2981) returns (bool)
    {
        return AbxBeaconCore.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
    }
}

A concrete token composes the mixins it needs and explicitly ORs each one's supportsInterface — Solady's leaf implementations don't super-chain, so composition is spelled out, not implicit, in the most-derived contract. ID is private on purpose: two extensions can never collide, even composed in the same token.

One extension branches its ERC-165 answer on state, deliberately: CreatorToken (opt-in ERC-721C) advertises the creator-token ids (0xad0d7f6c / 0xa07d229a) and its beacon version only when the token enrolled at deploy — a permanent, deploy-time choice — so an unenrolled token is indistinguishable from a pre-721C token.

Namespaced storage: no layout to protect

Every concern with real state — the beacon's extension registry, royalty, params, on-chain script, whatever — gets its own ERC-7201 namespace: a library with a Layout struct at a slot computed once, off any inheritance position.

library BeaconStorage {
    struct Layout {
        mapping(bytes32 => uint16) extensionVersion;
    }

    // keccak256(abi.encode(uint256(keccak256("abx.storage.beacon")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 internal constant STORAGE_SLOT = 0xfa76...;

    function layout() internal pure returns (Layout storage l) {
        assembly { l.slot := STORAGE_SLOT }
    }
}

Because the slot is a hash of the library's own name, not a position in an inheritance chain, mixins can be added, removed, or reordered on a concrete token with zero risk of two of them silently overlapping the same storage — "no reliance on inheritance-layout order anywhere in an ABX contract" is a repeated, literal comment across the codebase. It also enables the other half of the library story below: a shared, separately-deployed library can safely write into the calling token's own storage, because it computes the identical namespaced slot the token itself would.

Two kinds of libraries

  • Storage libraries (17: BeaconStorage, SupplyStorage, CollectionMetadataLib, TokenURIStorage, ContractURIStorage, ParamsStorage, ConfigurableParamsStorage, OnChainMetadataStorage, OnChainScriptStorage, DependenciesStorage, MaxInvocationsStorage, ExternalMinterStorage, PrimaryPayeeStorage, PausedStorage, SeedSourceStorage, SeriesMintStorage, TransferValidatorStorage) — nothing but a Layout struct and a layout() accessor. If a library has one of these, it holds state; every other library doesn't.
  • Logic libraries — stateless helpers, split by how they run:
    • Inlined (TokenDataLib, DynamicBuffer) — internal functions, compiled straight into the caller, no separate deployment. AbxVersion is a further degenerate case: a single constant, no functions at all.
    • Delegatecalled (AbxParamsLib, AbxCodeLib) — public functions, which Solidity always compiles as calls to a separately-deployed copy of the library (forge auto-deploys and links them). Params/ConfigurableParams route through AbxParamsLib; OnChainScript/Dependencies route through AbxCodeLib. Both are externalized for the same stated reason: "the spine is identical to an inlined implementation; EIP-170 is why it's a library"SeriesCode composes all four of these extensions at once, and inlining that much logic risks the 24,576-byte contract-size ceiling (the same ceiling that bounds one SSTORE2 chunk). AbxParamsLib also carries the params key-enumeration and schema read views: the mixin shells forward their raw calldata (same signature, same selector) and return the library's return data untouched, so the library's read signatures are part of the tokens' external ABI. Delegatecall preserves the caller's storage context, so the externalized logic still reads and writes the token's own namespaced state — never any of its own.

Even with both libraries externalized, SeriesCode still rides the ceiling: its implementation, its factory, and the on-chain generator singleton (AbxGenerator) all compile at a separately configured, lower optimizer setting (200 runs, against 1,000,000 everywhere else) purely to shrink further and fit. Every other contract optimizes for cheap runtime gas instead, on the reasoning that a clone, once deployed, runs forever.

Six contracts, three strict twins

The reference implementation ships one contract per (shape, standard) pair:

ShapeERC-721 (unique)ERC-1155 (copies)
One artworkOneOfOneImageOneOfOneEdition
Many artworksSeriesImageEditionImage
Generative / codeSeriesCodeEditionCode

Each column is a strict-superset ladder — a later contract is textually the earlier one's is (...) list with more mixins appended: OneOfOneImage/OneOfOneEdition compose the minimal set that can hold on-chain fields and a royalty; SeriesImage/EditionImage add what a sized, sellable drop needs — a supply cap, a delegatable minter, a payout address, a pause gate; SeriesCode/ EditionCode add what a generative drop needs on top of that — mint-time seeds, governed PostParams, and on-chain code custody. Each row is a twin pair sharing the identical extension surface, because every ABX extension mixin inherits AbxBeaconCore, not the ERC-721 or ERC-1155 base — nothing about an extension assumes a standard.

Every concrete contract's initialize() calls each mixin's _init<Name> in one fixed order, and the doc comment pins the exact resulting event sequence — for example SeriesCode's: AbxDeployed → AbxExtensionVersionSet(royalty) → RoyaltyChangedForAll → [AbxExtensionVersionSet(creator-token) → TransferValidatorUpdated] → … → AbxExtensionVersionSet(dependencies) → TokenFieldSet* → ContractURIUpdated → Transfer (the bracketed pair appears only when the token enrolls as a creator token at deploy). An indexer author can read the sequence straight off the source instead of inferring it from tests. EditionCode's sequence is the same shape, one rung over: TransferSingle in place of Transfer at the end, everything else identical.

What's shared, unchanged, between a twin pair

  • Every extension above the base itself: RoyaltyExtension, MaxInvocations (row 2+), ExternalMinter, PrimaryPayee, Paused, OnChainMetadata, SeedSourceExtension, ConfigurableParams, OnChainScript, Dependencies (row 3) — composed unchanged onto either base.
  • AbxMetadataRenderer, AbxGenerator, AbxChunkStore, AbxSeedSource — the shared, per-chain singletons — read a token only through its extension interfaces plus ICollectionName.name(), so the exact same deployed renderer serves a 721 token and its 1155 twin: no per-standard renderer, no redeploy for editions.
  • PostParams, except one leg. The TokenOwner auth leg generalizes from "the ownerOf holder" to "any holder with balanceOf(id) > 0" on an edition — params are per-id shared state of the artwork, and last-writer-wins among holders is the intended semantic — but delegate.xyz delegation does not carry over: an ERC-1155 id has no enumerable single holder to check a delegate against, so an edition's holders always configure directly, never through a vaulted delegate.

What differs

  • Per-id supply. An edition adds EditionSupplytotalSupply(id) / maxSupply(id) / setMaxSupply(id, cap) — the only extension id new to editions (abx.extension.edition-supply); every other extension id above is shared, unchanged, with the 721 side. MaxInvocations keeps its 721 meaning unchanged on the multi-id twins: it still caps the id space (how many distinct artworks may ever exist), not any one artwork's copy count.
  • The metadata-refresh signal. A 721 twin pings ERC-4906's MetadataUpdate; an edition has no such standard (IERC4906 is IERC721) and instead emits ERC-1155's native URI(string, uint256) per id — see Event spine.
  • The mint primitive. A 721 twin (past the 1/1) exposes IAbxSequentialMint; every edition exposes IAbxEditionMintmint(to, id, amount), an identified id with a per-mint amount, not a generalization of the sequential primitive. See Minting.
  • The creator-token flavor. CreatorToken (ERC-721C) and CreatorToken1155 (ERC-1155C) share the exact same ERC-165 ids and beacon extension id, but the validator call differs: 721C's is a view with no amount, 1155C's carries the transferred amount and isn't a view — called once per (id, amount) pair in a batch transfer. See Royalty enforcement.
  • The OneOfOneEdition sale-stack asymmetry. OneOfOneImage is one-shot and owner-only, with no sale stack at all. OneOfOneEdition ships ExternalMinter/PrimaryPayee/Paused from day one — a rung OneOfOneImage never climbs — because a priced open or limited edition of a single artwork is the dominant 1155 product, sold through its own sibling minter, AbxFixedPriceMinter1155.

One implementation, many clones

Each concrete token has one sibling factory (OneOfOneImageFactory, SeriesImageFactory, SeriesCodeFactory, and their edition twins OneOfOneEditionFactory, EditionImageFactory, EditionCodeFactory), and all six are the same shape:

  • The factory deploys one implementation in its own constructor, which immediately calls _disableInitializers() — so the master copy can never be initialized (or hijacked) directly.
  • deploy(params) clones it via EIP-1167 (Solady's LibClone.clone) and calls initialize(params) on the fresh clone in the same transaction.
  • deployDeterministic(params, salt) clones to a predictable address (LibClone.cloneDeterministic), guarded by the salt's leading 20 bytes — all-zero is permissionless, a specific address must match msg.sender — so a reserved address can't be front-run.
  • The factory is ownerless and immutable: no admin key, no upgrade path, nothing to rug. It is the only writer of its own isAbxClone mapping, which is the trust anchor — verifying a contract is canonical means verifying the factory deployed it, not trusting the spoofable AbxDeployed beacon (see Authenticity).

Renderers and readers: composed by address, not by inheritance

AbxMetadataRenderer, AbxChunkStore, AbxGenerator, and AbxSeedSource are a different idiom entirely: stateless, ownerless, shared singletons, deployed once per chain and referenced by address from a field's or a contract's stored value — never inherited into a token. A mixin composes in; a renderer, reader, or seed source composes alongside, staticcalled at read time (Field renderers), called through IAbxOnChainReader (On-chain storage), or called once at mint (Code projects). Keeping them outside the token's inheritance tree is what lets one deployment serve every token on the chain, of any concrete type, and be swapped out — by pointing a new address — without touching the token itself.

Event spine, Metadata, On-chain storage, Parameters, Code projects

On this page